1 // Written in the D programming language.
2 
3 /**
4  * 
5  * Tom's Obvious, Minimal Language (v0.4.0).
6  *
7  * License: $(HTTP https://github.com/Kripth/toml/blob/master/LICENSE, MIT)
8  * Authors: Kripth
9  * References: $(LINK https://github.com/toml-lang/toml/blob/master/README.md)
10  * Source: $(HTTP https://github.com/Kripth/toml/blob/master/src/toml/toml.d, toml/_toml.d)
11  * 
12  */
13 module toml.toml;
14 
15 import std.algorithm : canFind, min, stripRight;
16 import std.array : Appender;
17 import std.ascii : newline;
18 import std.conv : to;
19 import std.datetime : SysTime, DateTimeD = DateTime, Date, TimeOfDayD = TimeOfDay;
20 import std.exception : enforce, assertThrown;
21 import std.math : isNaN, isFinite;
22 import std.string : join, strip, replace, indexOf;
23 import std.traits : isNumeric, isIntegral, isFloatingPoint, isArray, isAssociativeArray, KeyType;
24 import std.typecons : Tuple;
25 import std.utf : encode, UseReplacementDchar;
26 
27 import toml.datetime : DateTime, TimeOfDay;
28 
29 /**
30  * Flags that control how a TOML document is parsed and encoded.
31  */
32 enum TOMLOptions {
33 
34 	none = 0x00,
35 	unquotedStrings = 0x01,		/// allow unquoted strings as values when parsing
36 
37 }
38 
39 /**
40  * TOML type enumeration.
41  */
42 enum TOML_TYPE : byte {
43 
44 	STRING,             /// Indicates the type of a TOMLValue.
45 	INTEGER,            /// ditto
46 	FLOAT,              /// ditto
47 	OFFSET_DATETIME,    /// ditto
48 	LOCAL_DATETIME,     /// ditto
49 	LOCAL_DATE,         /// ditto
50 	LOCAL_TIME,         /// ditto
51 	ARRAY,              /// ditto
52 	TABLE,              /// ditto
53 	TRUE,				/// ditto
54 	FALSE				/// ditto
55 
56 }
57 
58 /**
59  * Main table of a TOML document.
60  * It works as a TOMLValue with the TOML_TYPE.TABLE type.
61  */
62 struct TOMLDocument {
63 
64 	public TOMLValue[string] table;
65 
66 	public this(TOMLValue[string] table) {
67 		this.table = table;
68 	}
69 
70 	public this(TOMLValue value) {
71 		this(value.table);
72 	}
73 
74 	public string toString() {
75 		Appender!string appender;
76 		foreach(key, value; this.table) {
77 			appender.put(formatKey(key));
78 			appender.put(" = ");
79 			value.append(appender);
80 			appender.put(newline);
81 		}
82 		return appender.data;
83 	}
84 
85 	alias table this;
86 
87 }
88 
89 /**
90  * Value of a TOML value.
91  */
92 struct TOMLValue {
93 
94 	private union Store {
95 		string str;
96 		long integer;
97 		double floating;
98 		SysTime offsetDatetime;
99 		DateTime localDatetime;
100 		Date localDate;
101 		TimeOfDay localTime;
102 		TOMLValue[] array;
103 		TOMLValue[string] table;
104 	}
105 	private Store store;
106 	private TOML_TYPE _type;
107 
108 	public this(T)(T value) {
109 		static if(is(T == TOML_TYPE)) {
110 			this._type = value;
111 		} else {
112 			this.assign(value);
113 		}
114 	}
115 	
116 	public inout pure nothrow @property @safe @nogc TOML_TYPE type() {
117 		return this._type;
118 	}
119 	
120 	/**
121 	 * Throws: TOMLException if type is not TOML_TYPE.STRING
122 	 */
123 	public inout @property @trusted string str() {
124 		enforce!TOMLException(this._type == TOML_TYPE.STRING, "TOMLValue is not a string");
125 		return this.store.str;
126 	}
127 	
128 	/**
129 	 * Throws: TOMLException if type is not TOML_TYPE.INTEGER
130 	 */
131 	public inout @property @trusted long integer() {
132 		enforce!TOMLException(this._type == TOML_TYPE.INTEGER, "TOMLValue is not an integer");
133 		return this.store.integer;
134 	}
135 	
136 	/**
137 	 * Throws: TOMLException if type is not TOML_TYPE.FLOAT
138 	 */
139 	public inout @property @trusted double floating() {
140 		enforce!TOMLException(this._type == TOML_TYPE.FLOAT, "TOMLValue is not a float");
141 		return this.store.floating;
142 	}
143 	
144 	/**
145 	 * Throws: TOMLException if type is not TOML_TYPE.OFFSET_DATETIME
146 	 */
147 	public @property ref SysTime offsetDatetime() {
148 		enforce!TOMLException(this.type == TOML_TYPE.OFFSET_DATETIME, "TOMLValue is not an offset datetime");
149 		return this.store.offsetDatetime;
150 	}
151 	
152 	/**
153 	 * Throws: TOMLException if type is not TOML_TYPE.LOCAL_DATETIME
154 	 */
155 	public @property @trusted ref DateTime localDatetime() {
156 		enforce!TOMLException(this._type == TOML_TYPE.LOCAL_DATETIME, "TOMLValue is not a local datetime");
157 		return this.store.localDatetime;
158 	}
159 	
160 	/**
161 	 * Throws: TOMLException if type is not TOML_TYPE.LOCAL_DATE
162 	 */
163 	public @property @trusted ref Date localDate() {
164 		enforce!TOMLException(this._type == TOML_TYPE.LOCAL_DATE, "TOMLValue is not a local date");
165 		return this.store.localDate;
166 	}
167 	
168 	/**
169 	 * Throws: TOMLException if type is not TOML_TYPE.LOCAL_TIME
170 	 */
171 	public @property @trusted ref TimeOfDay localTime() {
172 		enforce!TOMLException(this._type == TOML_TYPE.LOCAL_TIME, "TOMLValue is not a local time");
173 		return this.store.localTime;
174 	}
175 	
176 	/**
177 	 * Throws: TOMLException if type is not TOML_TYPE.ARRAY
178 	 */
179 	public @property @trusted ref TOMLValue[] array() {
180 		enforce!TOMLException(this._type == TOML_TYPE.ARRAY, "TOMLValue is not an array");
181 		return this.store.array;
182 	}
183 	
184 	/**
185 	 * Throws: TOMLException if type is not TOML_TYPE.TABLE
186 	 */
187 	public @property @trusted ref TOMLValue[string] table() {
188 		enforce!TOMLException(this._type == TOML_TYPE.TABLE, "TOMLValue is not a table");
189 		return this.store.table;
190 	}
191 
192 	public TOMLValue opIndex(size_t index) {
193 		return this.array[index];
194 	}
195 
196 	public TOMLValue* opBinaryRight(string op : "in")(string key) {
197 		return key in this.table;
198 	}
199 
200 	public TOMLValue opIndex(string key) {
201 		return this.table[key];
202 	}
203 
204 	public int opApply(scope int delegate(string, ref TOMLValue) dg) {
205 		enforce!TOMLException(this._type == TOML_TYPE.TABLE, "TOMLValue is not a table");
206 		int result;
207 		foreach(string key, ref value; this.store.table) {
208 			result = dg(key, value);
209 			if(result) break;
210 		}
211 		return result;
212 	}
213 
214 	public void opAssign(T)(T value) {
215 		this.assign(value);
216 	}
217 
218 	private void assign(T)(T value) {
219 		static if(is(T == TOMLValue)) {
220 			this.store = value.store;
221 			this._type = value._type;
222 		} else static if(is(T : string)) {
223 			this.store.str = value;
224 			this._type = TOML_TYPE.STRING;
225 		} else static if(isIntegral!T) {
226 			this.store.integer = value;
227 			this._type = TOML_TYPE.INTEGER;
228 		} else static if(isFloatingPoint!T) {
229 			this.store.floating = value.to!double;
230 			this._type = TOML_TYPE.FLOAT;
231 		} else static if(is(T == SysTime)) {
232 			this.store.offsetDatetime = value;
233 			this._type = TOML_TYPE.OFFSET_DATETIME;
234 		} else static if(is(T == DateTime)) {
235 			this.store.localDatetime = value;
236 			this._type = TOML_TYPE.LOCAL_DATETIME;
237 		} else static if(is(T == DateTimeD)) {
238 			this.store.localDatetime = DateTime(value.date, TimeOfDay(value.timeOfDay));
239 			this._type = TOML_TYPE.LOCAL_DATETIME;
240 		} else static if(is(T == Date)) {
241 			this.store.localDate = value;
242 			this._type = TOML_TYPE.LOCAL_DATE;
243 		} else static if(is(T == TimeOfDay)) {
244 			this.store.localTime = value;
245 			this._type = TOML_TYPE.LOCAL_TIME;
246 		} else static if(is(T == TimeOfDayD)) {
247 			this.store.localTime = TimeOfDay(value);
248 			this._type = TOML_TYPE.LOCAL_TIME;
249 		} else static if(isArray!T) {
250 			static if(is(T == TOMLValue[])) {
251 				if(value.length) {
252 					// verify that every element has the same type
253 					TOML_TYPE cmp = value[0].type;
254 					foreach(element ; value[1..$]) {
255 						enforce!TOMLException(element.type == cmp, "Array's values must be of the same type");
256 					}
257 				}
258 				alias data = value;
259 			} else {
260 				TOMLValue[] data;
261 				foreach(element ; value) {
262 					data ~= TOMLValue(element);
263 				}
264 			}
265 			this.store.array = data;
266 			this._type = TOML_TYPE.ARRAY;
267 		} else static if(isAssociativeArray!T && is(KeyType!T : string)) {
268 			static if(is(T == TOMLValue[string])) {
269 				alias data = value;
270 			} else {
271 				TOMLValue[string] data;
272 				foreach(key, v; value) {
273 					data[key] = v;
274 				}
275 			}
276 			this.store.table = data;
277 			this._type = TOML_TYPE.TABLE;
278 		} else static if(is(T == bool)) {
279 			_type = value ? TOML_TYPE.TRUE : TOML_TYPE.FALSE;
280 		} else {
281 			static assert(0);
282 		}
283 	}
284 
285 	public bool opEquals(T)(T value) {
286 		static if(is(T == TOMLValue)) {
287 			if(this._type != value._type) return false;
288 			final switch(this.type) with(TOML_TYPE) {
289 				case STRING: return this.store.str == value.store.str;
290 				case INTEGER: return this.store.integer == value.store.integer;
291 				case FLOAT: return this.store.floating == value.store.floating;
292 				case OFFSET_DATETIME: return this.store.offsetDatetime == value.store.offsetDatetime;
293 				case LOCAL_DATETIME: return this.store.localDatetime == value.store.localDatetime;
294 				case LOCAL_DATE: return this.store.localDate == value.store.localDate;
295 				case LOCAL_TIME: return this.store.localTime == value.store.localTime;
296 				case ARRAY: return this.store.array == value.store.array;
297 				//case TABLE: return this.store.table == value.store.table; // causes errors
298 				case TABLE: return this.opEquals(value.store.table);
299 				case TRUE: case FALSE: return true;
300 			}
301 		} else static if(is(T : string)) {
302 			return this._type == TOML_TYPE.STRING && this.store.str == value;
303 		} else static if(isNumeric!T ) {
304 			if(this._type == TOML_TYPE.INTEGER) return this.store.integer == value;
305 			else if(this._type == TOML_TYPE.FLOAT) return this.store.floating == value;
306 			else return false;
307 		} else static if(is(T == SysTime)) {
308 			return this._type == TOML_TYPE.OFFSET_DATETIME && this.store.offsetDatetime == value;
309 		} else static if(is(T == DateTime)) {
310 			return this._type == TOML_TYPE.LOCAL_DATETIME && this.store.localDatetime.dateTime == value.dateTime && this.store.localDatetime.timeOfDay.fracSecs == value.timeOfDay.fracSecs;
311 		} else static if(is(T == DateTimeD)) {
312 			return this._type == TOML_TYPE.LOCAL_DATETIME && this.store.localDatetime.dateTime == value;
313 		} else static if(is(T == Date)) {
314 			return this._type == TOML_TYPE.LOCAL_DATE && this.store.localDate == value;
315 		} else static if(is(T == TimeOfDay)) {
316 			return this._type == TOML_TYPE.LOCAL_TIME && this.store.localTime.timeOfDay == value.timeOfDay && this.store.localTime.fracSecs == value.fracSecs;
317 		} else static if(is(T == TimeOfDayD)) {
318 			return this._type == TOML_TYPE.LOCAL_TIME && this.store.localTime == value;
319 		} else static if(isArray!T) {
320 			if(this._type != TOML_TYPE.ARRAY || this.store.array.length != value.length) return false;
321 			foreach(i, element; this.store.array) {
322 				if(element != value[i]) return false;
323 			}
324 			return true;
325 		} else static if(isAssociativeArray!T && is(KeyType!T : string)) {
326 			if(this._type != TOML_TYPE.TABLE || this.store.table.length != value.length) return false;
327 			foreach(key, v; this.store.table) {
328 				auto cmp = key in value;
329 				if(cmp is null || v != *cmp) return false;
330 			}
331 			return true;
332 		} else static if(is(T == bool)) {
333 			return value ? _type == TOML_TYPE.TRUE : _type == TOML_TYPE.FALSE;
334 		} else {
335 			return false;
336 		}
337 	}
338 
339 	public inout void append(ref Appender!string appender) {
340 		final switch(this._type) with(TOML_TYPE) {
341 			case STRING:
342 				appender.put(formatString(this.store.str));
343 				break;
344 			case INTEGER:
345 				appender.put(this.store.integer.to!string);
346 				break;
347 			case FLOAT:
348 				immutable str = this.store.floating.to!string;
349 				appender.put(str);
350 				if(!str.canFind('.') && !str.canFind('e')) appender.put(".0");
351 				break;
352 			case OFFSET_DATETIME:
353 				appender.put(this.store.offsetDatetime.toISOExtString());
354 				break;
355 			case LOCAL_DATETIME:
356 				appender.put(this.store.localDatetime.toISOExtString());
357 				break;
358 			case LOCAL_DATE:
359 				appender.put(this.store.localDate.toISOExtString());
360 				break;
361 			case LOCAL_TIME:
362 				appender.put(this.store.localTime.toISOExtString());
363 				break;
364 			case ARRAY:
365 				appender.put("[");
366 				foreach(i, value; this.store.array) {
367 					value.append(appender);
368 					if(i + 1 < this.store.array.length) appender.put(", ");
369 				}
370 				appender.put("]");
371 				break;
372 			case TABLE:
373 				// display as an inline table
374 				appender.put("{ ");
375 				size_t i = 0;
376 				foreach(key, value; this.store.table) {
377 					appender.put(formatKey(key));
378 					appender.put(" = ");
379 					value.append(appender);
380 					if(++i != this.store.table.length) appender.put(", ");
381 				}
382 				appender.put(" }");
383 				break;
384 			case TRUE:
385 				appender.put("true");
386 				break;
387 			case FALSE:
388 				appender.put("false");
389 				break;
390 		}
391 	}
392 
393 	public inout string toString() {
394 		Appender!string appender;
395 		this.append(appender);
396 		return appender.data;
397 	}
398 
399 }
400 
401 private string formatKey(string str) {
402 	foreach(c ; str) {
403 		if((c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && c != '-' && c != '_') return formatString(str);
404 	}
405 	return str;
406 }
407 
408 private string formatString(string str) {
409 	Appender!string appender;
410 	foreach(c ; str) {
411 		switch(c) {
412 			case '"': appender.put("\\\""); break;
413 			case '\\': appender.put("\\\\"); break;
414 			case '\b': appender.put("\\b"); break;
415 			case '\f': appender.put("\\f"); break;
416 			case '\n': appender.put("\\n"); break;
417 			case '\r': appender.put("\\r"); break;
418 			case '\t': appender.put("\\t"); break;
419 			default: appender.put(c);
420 		}
421 	}
422 	return "\"" ~ appender.data ~ "\"";
423 }
424 
425 /**
426  * Parses a TOML document.
427  * Returns: a TOMLDocument with the parsed data
428  * Throws:
429  * 		TOMLParserException when the document's syntax is incorrect
430  */
431 TOMLDocument parseTOML(string data, TOMLOptions options=TOMLOptions.none) {
432 	
433 	size_t index = 0;
434 
435 	/**
436 	 * Throws a TOMLParserException at the current line and column.
437 	 */
438 	void error(string message) {
439 		if(index >= data.length) index = data.length;
440 		size_t i, line, column;
441 		while(i < index) {
442 			if(data[i++] == '\n') {
443 				line++;
444 				column = 0;
445 			} else {
446 				column++;
447 			}
448 		}
449 		throw new TOMLParserException(message, line + 1, column);
450 	}
451 
452 	/**
453 	 * Throws a TOMLParserException throught the error function if
454 	 * cond is false.
455 	 */
456 	void enforceParser(bool cond, lazy string message) {
457 		if(!cond) {
458 			error(message);
459 		}
460 	}
461 
462 	TOMLValue[string] _ret;
463 	auto current = &_ret;
464 
465 	string[][] tableNames;
466 
467 	void setImpl(TOMLValue[string]* table, string[] keys, string[] original, TOMLValue value) {
468 		auto ptr = keys[0] in *table;
469 		if(keys.length == 1) {
470 			// should not be there
471 			enforceParser(ptr is null, "Key is already defined");
472 			(*table)[keys[0]] = value;
473 		} else {
474 			// must be a table
475 			if(ptr !is null) enforceParser((*ptr).type == TOML_TYPE.TABLE, join(original[0..$-keys.length], ".") ~ " is already defined and is not a table");
476 			else (*table)[keys[0]] = (TOMLValue[string]).init;
477 			setImpl(&((*table)[keys[0]].table()), keys[1..$], original, value);
478 		}
479 	}
480 
481 	void set(string[] keys, TOMLValue value) {
482 		setImpl(current, keys, keys, value);
483 	}
484 
485 	/**
486 	 * Removes whitespace characters and comments.
487 	 * Return: whether there's still data to read
488 	 */
489 	bool clear(bool clear_newline=true)() {
490 		static if(clear_newline) {
491 			enum chars = " \t\r\n";
492 		} else {
493 			enum chars = " \t\r";
494 		}
495 		if(index < data.length) {
496 			if(chars.canFind(data[index])) {
497 				index++;
498 				return clear!clear_newline();
499 			} else if(data[index] == '#') {
500 				// skip until end of line
501 				while(++index < data.length && data[index] != '\n') {}
502 				static if(clear_newline) {
503 					index++; // point at the next character
504 					return clear();
505 				} else {
506 					return true;
507 				}
508 			} else {
509 				return true;
510 			}
511 		} else {
512 			return false;
513 		}
514 	}
515 
516 	/**
517 	 * Indicates whether the given character is valid in an unquoted key.
518 	 */
519 	bool isValidKeyChar(immutable char c) {
520 		return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_';
521 	}
522 
523 	string readQuotedString(bool multiline)() {
524 		Appender!string ret;
525 		bool backslash = false;
526 		while(index < data.length) {
527 			static if(!multiline) {
528 				enforceParser(data[index] != '\n', "Unterminated quoted string");
529 			}
530 			if(backslash) {
531 				void readUnicode(size_t size)() {
532 					enforceParser(index + size < data.length, "Invalid UTF-8 sequence");
533 					char[4] buffer;
534 					immutable len = encode!(UseReplacementDchar.yes)(buffer, cast(dchar)to!ulong(data[index+1..index+1+size], 16));
535 					ret.put(buffer[0..len].idup);
536 					index += size;
537 				}
538 				switch(data[index]) {
539 					case '"': ret.put('"'); break;
540 					case '\\': ret.put('\\'); break;
541 					case 'b': ret.put('\b'); break;
542 					case 't': ret.put('\t'); break;
543 					case 'n': ret.put('\n'); break;
544 					case 'f': ret.put('\f'); break;
545 					case 'r': ret.put('\r'); break;
546 					case 'u': readUnicode!4(); break;
547 					case 'U': readUnicode!8(); break;
548 					default:
549 						static if(multiline) {
550 							index++;
551 							if(clear()) {
552 								// remove whitespace characters until next valid character
553 								index--;
554 								break;
555 							}
556 						}
557 						enforceParser(false, "Invalid escape sequence: '\\" ~ (index < data.length ? [data[index]] : "EOF") ~ "'");
558 				}
559 				backslash = false;
560 			} else {
561 				if(data[index] == '\\') {
562 					backslash = true;
563 				} else if(data[index] == '"') {
564 					// string closed
565 					index++;
566 					static if(multiline) {
567 						// control that the string is really closed
568 						if(index + 2 <= data.length && data[index..index+2] == "\"\"") {
569 							index += 2;
570 							return ret.data.stripFirstLine;
571 						} else {
572 							ret.put("\"");
573 							continue;
574 						}
575 					} else {
576 						return ret.data;
577 					}
578 				} else {
579 					static if(multiline) {
580 						mixin(doLineConversion);
581 					}
582 					ret.put(data[index]);
583 				}
584 			}
585 			index++;
586 		}
587 		error("Expecting \" (double quote) but found EOF");	assert(0);
588 	}
589 
590 	string readSimpleQuotedString(bool multiline)() {
591 		Appender!string ret;
592 		while(index < data.length) {
593 			static if(!multiline) {
594 				enforceParser(data[index] != '\n', "Unterminated quoted string");
595 			}
596 			if(data[index] == '\'') {
597 				// closed
598 				index++;
599 				static if(multiline) {
600 					// there must be 3 of them
601 					if(index + 2 <= data.length && data[index..index+2] == "''") {
602 						index += 2;
603 						return ret.data.stripFirstLine;
604 					} else {
605 						ret.put("'");
606 					}
607 				} else {
608 					return ret.data;
609 				}
610 			} else {
611 				static if(multiline) {
612 					mixin(doLineConversion);
613 				}
614 				ret.put(data[index++]);
615 			}
616 		}
617 		error("Expecting ' (single quote) but found EOF"); assert(0);
618 	}
619 
620 	string removeUnderscores(string str, string[] ranges...) {
621 		bool checkRange(char c) {
622 			foreach(range ; ranges) {
623 				if(c >= range[0] && c <= range[1]) return true;
624 			}
625 			return false;
626 		}
627 		bool underscore = false;
628 		for(size_t i=0; i<str.length; i++) {
629 			if(str[i] == '_') {
630 				if(underscore || i == 0 || i == str.length - 1 || !checkRange(str[i-1]) || !checkRange(str[i+1])) throw new Exception("");
631 				str = str[0..i] ~ str[i+1..$];
632 				i--;
633 				underscore = true;
634 			} else {
635 				underscore = false;
636 			}
637 		}
638 		return str;
639 	}
640 
641 	TOMLValue readSpecial() {
642 		immutable start = index;
643 		while(index < data.length && !"\t\r\n,]}#".canFind(data[index])) index++;
644 		string ret = data[start..index].stripRight(' ');
645 		enforceParser(ret.length > 0, "Invalid empty value");
646 		switch(ret) {
647 			case "true":
648 				return TOMLValue(true);
649 			case "false":
650 				return TOMLValue(false);
651 			case "inf":
652 			case "+inf":
653 				return TOMLValue(double.infinity);
654 			case "-inf":
655 				return TOMLValue(-double.infinity);
656 			case "nan":
657 			case "+nan":
658 				return TOMLValue(double.nan);
659 			case "-nan":
660 				return TOMLValue(-double.nan);
661 			default:
662 				immutable original = ret;
663 				try {
664 					if(ret.length >= 10 && ret[4] == '-' && ret[7] == '-') {
665 						// date or datetime
666 						if(ret.length >= 19 && (ret[10] == 'T' || ret[10] == ' ') && ret[13] == ':' && ret[16] == ':') {
667 							// datetime
668 							if(ret[10] == ' ') ret = ret[0..10] ~ 'T' ~ ret[11..$];
669 							if(ret[19..$].canFind("-") || ret[$-1] == 'Z') {
670 								// has timezone
671 								return TOMLValue(SysTime.fromISOExtString(ret));
672 							} else {
673 								// is space allowed instead of T?
674 								return TOMLValue(DateTime.fromISOExtString(ret));
675 							}
676 						} else {
677 							return TOMLValue(Date.fromISOExtString(ret));
678 						}
679 					} else if(ret.length >= 8 && ret[2] == ':' && ret[5] == ':') {
680 						return TOMLValue(TimeOfDay.fromISOExtString(ret));
681 					}
682 					if(ret.length > 2 && ret[0] == '0') {
683 						switch(ret[1]) {
684 							case 'x': return TOMLValue(to!long(removeUnderscores(ret[2..$], "09", "AZ", "az"), 16));
685 							case 'o': return TOMLValue(to!long(removeUnderscores(ret[2..$], "08"), 8));
686 							case 'b': return TOMLValue(to!long(removeUnderscores(ret[2..$], "01"), 2));
687 							default: break;
688 						}
689 					}
690 					if(ret.canFind('.') || ret.canFind('e') || ret.canFind('E')) {
691 						return TOMLValue(to!double(removeUnderscores(ret, "09")));
692 					} else {
693 						if(ret[0] != '0' || ret.length == 1) return TOMLValue(to!long(removeUnderscores(ret, "09")));
694 					}
695 				} catch(Exception) {}
696 				// not a valid value at this point
697 				if(options & TOMLOptions.unquotedStrings) return TOMLValue(original);
698 				else error("Invalid type: '" ~ original ~ "'"); assert(0);
699 		}
700 	}
701 	
702 	string readKey() {
703 		enforceParser(index < data.length, "Key declaration expected but found EOF");
704 		string ret;
705 		if(data[index] == '"') {
706 			index++;
707 			ret = readQuotedString!false();
708 		} else if(data[index] == '\'') {
709 			index++;
710 			ret = readSimpleQuotedString!false();
711 		} else {
712 			Appender!string appender;
713 			while(index < data.length && isValidKeyChar(data[index])) {
714 				appender.put(data[index++]);
715 			}
716 			ret = appender.data;
717 			enforceParser(ret.length != 0, "Key is empty or contains invalid characters");
718 		}
719 		return ret;
720 	}
721 	
722 	string[] readKeys() {
723 		string[] keys;
724 		index--;
725 		do {
726 			index++;
727 			clear!false();
728 			keys ~= readKey();
729 			clear!false();
730 		} while(index < data.length && data[index] == '.');
731 		enforceParser(keys.length != 0, "Key cannot be empty");
732 		return keys;
733 	}
734 
735 	TOMLValue readValue() {
736 		if(index < data.length) {
737 			switch(data[index++]) {
738 				case '"':
739 					if(index + 2 <= data.length && data[index..index+2] == "\"\"") {
740 						index += 2;
741 						return TOMLValue(readQuotedString!true());
742 					} else {
743 						return TOMLValue(readQuotedString!false());
744 					}
745 				case '\'':
746 					if(index + 2 <= data.length && data[index..index+2] == "''") {
747 						index += 2;
748 						return TOMLValue(readSimpleQuotedString!true());
749 					} else {
750 						return TOMLValue(readSimpleQuotedString!false());
751 					}
752 				case '[':
753 					clear();
754 					TOMLValue[] array;
755 					bool comma = true;
756 					while(data[index] != ']') { //TODO check range error
757 						enforceParser(comma, "Elements of the array must be separated with a comma");
758 						array ~= readValue();
759 						clear!false(); // spaces allowed between elements and commas
760 						if(data[index] == ',') { //TODO check range error
761 							index++;
762 							comma = true;
763 						} else {
764 							comma = false;
765 						}
766 						clear(); // spaces and newlines allowed between elements
767 					}
768 					index++;
769 					return TOMLValue(array);
770 				case '{':
771 					clear!false();
772 					TOMLValue[string] table;
773 					bool comma = true;
774 					while(data[index] != '}') { //TODO check range error
775 						enforceParser(comma, "Elements of the table must be separated with a comma");
776 						auto keys = readKeys();
777 						enforceParser(clear!false() && data[index++] == '=' && clear!false(), "Expected value after key declaration");
778 						setImpl(&table, keys, keys, readValue());
779 						enforceParser(clear!false(), "Expected ',' or '}' but found " ~ (index < data.length ? "EOL" : "EOF"));
780 						if(data[index] == ',') {
781 							index++;
782 							comma = true;
783 						} else {
784 							comma = false;
785 						}
786 						clear!false();
787 					}
788 					index++;
789 					return TOMLValue(table);
790 				default:
791 					index--;
792 					break;
793 			}
794 		}
795 		return readSpecial();
796 	}
797 
798 	void readKeyValue(string[] keys) {
799 		if(clear()) {
800 			enforceParser(data[index++] == '=', "Expected '=' after key declaration");
801 			if(clear!false()) {
802 				set(keys, readValue());
803 				// there must be nothing after the key/value declaration except comments and whitespaces
804 				if(clear!false()) enforceParser(data[index] == '\n', "Invalid characters after value declaration: " ~ data[index]);
805 			} else {
806 				//TODO throw exception (missing value)
807 			}
808 		} else {
809 			//TODO throw exception (missing value)
810 		}
811 	}
812 	
813 	void next() {
814 
815 		if(data[index] == '[') {
816 			current = &_ret; // reset base
817 			index++;
818 			bool array = false;
819 			if(index < data.length && data[index] == '[') {
820 				index++;
821 				array = true;
822 			}
823 			string[] keys = readKeys();
824 			enforceParser(index < data.length && data[index++] == ']', "Invalid " ~ (array ? "array" : "table") ~ " key declaration");
825 			if(array) enforceParser(index < data.length && data[index++] == ']', "Invalid array key declaration");
826 			if(!array) {
827 				//TODO only enforce if every key is a table
828 				enforceParser(!tableNames.canFind(keys), "Table name has already been directly defined");
829 				tableNames ~= keys;
830 			}
831 			void update(string key, bool allowArray=true) {
832 				if(key !in *current) set([key], TOMLValue(TOML_TYPE.TABLE));
833 				auto ret = (*current)[key];
834 				if(ret.type == TOML_TYPE.TABLE) current = &((*current)[key].table());
835 				else if(allowArray && ret.type == TOML_TYPE.ARRAY) current = &((*current)[key].array[$-1].table());
836 				else error("Invalid type");
837 			}
838 			foreach(immutable key ; keys[0..$-1]) {
839 				update(key);
840 			}
841 			if(array) {
842 				auto exist = keys[$-1] in *current;
843 				if(exist) {
844 					//TODO must be an array
845 					(*exist).array ~= TOMLValue(TOML_TYPE.TABLE);
846 				} else {
847 					set([keys[$-1]], TOMLValue([TOMLValue(TOML_TYPE.TABLE)]));
848 				}
849 				current = &((*current)[keys[$-1]].array[$-1].table());
850 			} else {
851 				update(keys[$-1], false);
852 			}
853 		} else {
854 			readKeyValue(readKeys());
855 		}
856 
857 	}
858 
859 	while(clear()) {
860 		next();
861 	}
862 
863 	return TOMLDocument(_ret);
864 
865 }
866 
867 private @property string stripFirstLine(string data) {
868 	size_t i = 0;
869 	while(i < data.length && data[i] != '\n') i++;
870 	if(data[0..i].strip.length == 0) return data[i+1..$];
871 	else return data;
872 }
873 
874 version(Windows) {
875 	// convert posix's line ending to windows'
876 	private enum doLineConversion = q{
877 		if(data[index] == '\n' && index != 0 && data[index-1] != '\r') {
878 			index++;
879 			ret.put("\r\n");
880 			continue;
881 		}
882 	};
883 } else {
884 	// convert windows' line ending to posix's
885 	private enum doLineConversion = q{
886 		if(data[index] == '\r' && index + 1 < data.length && data[index+1] == '\n') {
887 			index += 2;
888 			ret.put("\n");
889 			continue;
890 		}
891 	};
892 }
893 
894 unittest {
895 
896 	TOMLDocument doc;
897 
898 	// tests from the official documentation
899 	// https://github.com/toml-lang/toml/blob/master/README.md
900 
901 	doc = parseTOML(`
902 		# This is a TOML document.
903 
904 		title = "TOML Example"
905 
906 		[owner]
907 		name = "Tom Preston-Werner"
908 		dob = 1979-05-27T07:32:00-08:00 # First class dates
909 
910 		[database]
911 		server = "192.168.1.1"
912 		ports = [ 8001, 8001, 8002 ]
913 		connection_max = 5000
914 		enabled = true
915 
916 		[servers]
917 
918 		  # Indentation (tabs and/or spaces) is allowed but not required
919 		  [servers.alpha]
920 		  ip = "10.0.0.1"
921 		  dc = "eqdc10"
922 
923 		  [servers.beta]
924 		  ip = "10.0.0.2"
925 		  dc = "eqdc10"
926 
927 		[clients]
928 		data = [ ["gamma", "delta"], [1, 2] ]
929 
930 		# Line breaks are OK when inside arrays
931 		hosts = [
932 		  "alpha",
933 		  "omega"
934 		]
935 	`);
936 	assert(doc["title"] == "TOML Example");
937 	assert(doc["owner"]["name"] == "Tom Preston-Werner");
938 	assert(doc["owner"]["dob"] == SysTime.fromISOExtString("1979-05-27T07:32:00-08:00"));
939 	assert(doc["database"]["server"] == "192.168.1.1");
940 	assert(doc["database"]["ports"] == [8001, 8001, 8002]);
941 	assert(doc["database"]["connection_max"] == 5000);
942 	assert(doc["database"]["enabled"] == true);
943 	//TODO
944 	assert(doc["clients"]["data"][0] == ["gamma", "delta"]);
945 	assert(doc["clients"]["data"][1] == [1, 2]);
946 	assert(doc["clients"]["hosts"] == ["alpha", "omega"]);
947 
948 	doc = parseTOML(`
949 		# This is a full-line comment
950 		key = "value"
951 	`);
952 	assert("key" in doc);
953 	assert(doc["key"].type == TOML_TYPE.STRING);
954 	assert(doc["key"].str == "value");
955 
956 	foreach (k, v; doc) {
957 		assert(k == "key");
958 		assert(v.type == TOML_TYPE.STRING);
959 		assert(v.str == "value");
960 	}
961 
962 	assertThrown!TOMLException({ parseTOML(`key = # INVALID`); }());
963 	assertThrown!TOMLException({ parseTOML("key =\nkey2 = 'test'"); }());
964 
965 	// ----
966 	// Keys
967 	// ----
968 
969 	// bare keys
970 	doc = parseTOML(`
971 		key = "value"
972 		bare_key = "value"
973 		bare-key = "value"
974 		1234 = "value"
975 	`);
976 	assert(doc["key"] == "value");
977 	assert(doc["bare_key"] == "value");
978 	assert(doc["bare-key"] == "value");
979 	assert(doc["1234"] == "value");
980 
981 	// quoted keys
982 	doc = parseTOML(`
983 		"127.0.0.1" = "value"
984 		"character encoding" = "value"
985 		"ʎǝʞ" = "value"
986 		'key2' = "value"
987 		'quoted "value"' = "value"
988 	`);
989 	assert(doc["127.0.0.1"] == "value");
990 	assert(doc["character encoding"] == "value");
991 	assert(doc["ʎǝʞ"] == "value");
992 	assert(doc["key2"] == "value");
993 	assert(doc["quoted \"value\""] == "value");
994 
995 	// no key name
996 	assertThrown!TOMLException({ parseTOML(`= "no key name" # INVALID`); }());
997 
998 	// empty key
999 	assert(parseTOML(`"" = "blank"`)[""] == "blank");
1000 	assert(parseTOML(`'' = 'blank'`)[""] == "blank");
1001 
1002 	// dotted keys
1003 	doc = parseTOML(`
1004 		name = "Orange"
1005 		physical.color = "orange"
1006 		physical.shape = "round"
1007 		site."google.com" = true
1008 	`);
1009 	assert(doc["name"] == "Orange");
1010 	assert(doc["physical"] == ["color": "orange", "shape": "round"]);
1011 	assert(doc["site"]["google.com"] == true);
1012 
1013 	// ------
1014 	// String
1015 	// ------
1016 
1017 	// basic strings
1018 	doc = parseTOML(`str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."`);
1019 	assert(doc["str"] == "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF.");
1020 
1021 	// multi-line basic strings
1022 	doc = parseTOML(`str1 = """
1023 Roses are red
1024 Violets are blue"""`);
1025 	version(Posix) assert(doc["str1"] == "Roses are red\nViolets are blue");
1026 	else assert(doc["str1"] == "Roses are red\r\nViolets are blue");
1027 
1028 	doc = parseTOML(`
1029 		# The following strings are byte-for-byte equivalent:
1030 		str1 = "The quick brown fox jumps over the lazy dog."
1031 		
1032 		str2 = """
1033 The quick brown \
1034 
1035 
1036 		  fox jumps over \
1037 		    the lazy dog."""
1038 		
1039 		str3 = """\
1040 			The quick brown \
1041 			fox jumps over \
1042 			the lazy dog.\
1043        """`);
1044 	assert(doc["str1"] == "The quick brown fox jumps over the lazy dog.");
1045 	assert(doc["str1"] == doc["str2"]);
1046 	assert(doc["str1"] == doc["str3"]);
1047 
1048 	// literal strings
1049 	doc = parseTOML(`
1050 		# What you see is what you get.
1051 		winpath  = 'C:\Users\nodejs\templates'
1052 		winpath2 = '\\ServerX\admin$\system32\'
1053 		quoted   = 'Tom "Dubs" Preston-Werner'
1054 		regex    = '<\i\c*\s*>'
1055 	`);
1056 	assert(doc["winpath"] == `C:\Users\nodejs\templates`);
1057 	assert(doc["winpath2"] == `\\ServerX\admin$\system32\`);
1058 	assert(doc["quoted"] == `Tom "Dubs" Preston-Werner`);
1059 	assert(doc["regex"] == `<\i\c*\s*>`);
1060 
1061 	// multi-line literal strings
1062 	doc = parseTOML(`
1063 		regex2 = '''I [dw]on't need \d{2} apples'''
1064 		lines  = '''
1065 The first newline is
1066 trimmed in raw strings.
1067    All other whitespace
1068    is preserved.
1069 '''`);
1070 	assert(doc["regex2"] == `I [dw]on't need \d{2} apples`);
1071 	assert(doc["lines"] == "The first newline is" ~ newline ~ "trimmed in raw strings." ~ newline ~ "   All other whitespace" ~ newline ~ "   is preserved." ~ newline);
1072 
1073 	// -------
1074 	// Integer
1075 	// -------
1076 
1077 	doc = parseTOML(`
1078 		int1 = +99
1079 		int2 = 42
1080 		int3 = 0
1081 		int4 = -17
1082 	`);
1083 	assert(doc["int1"].type == TOML_TYPE.INTEGER);
1084 	assert(doc["int1"].integer == 99);
1085 	assert(doc["int2"] == 42);
1086 	assert(doc["int3"] == 0);
1087 	assert(doc["int4"] == -17);
1088 
1089 	doc = parseTOML(`
1090 		int5 = 1_000
1091 		int6 = 5_349_221
1092 		int7 = 1_2_3_4_5     # VALID but discouraged
1093 	`);
1094 	assert(doc["int5"] == 1_000);
1095 	assert(doc["int6"] == 5_349_221);
1096 	assert(doc["int7"] == 1_2_3_4_5);
1097 
1098 	// leading 0s not allowed
1099 	assertThrown!TOMLException({ parseTOML(`invalid = 01`); }());
1100 
1101 	// underscores must be enclosed in numbers
1102 	assertThrown!TOMLException({ parseTOML(`invalid = _123`); }());
1103 	assertThrown!TOMLException({ parseTOML(`invalid = 123_`); }());
1104 	assertThrown!TOMLException({ parseTOML(`invalid = 123__123`); }());
1105 	assertThrown!TOMLException({ parseTOML(`invalid = 0b01_21`); }());
1106 	assertThrown!TOMLException({ parseTOML(`invalid = 0x_deadbeef`); }());
1107 	assertThrown!TOMLException({ parseTOML(`invalid = 0b0101__00`); }());
1108 
1109 	doc = parseTOML(`
1110 		# hexadecimal with prefix 0x
1111 		hex1 = 0xDEADBEEF
1112 		hex2 = 0xdeadbeef
1113 		hex3 = 0xdead_beef
1114 
1115 		# octal with prefix 0o
1116 		oct1 = 0o01234567
1117 		oct2 = 0o755 # useful for Unix file permissions
1118 
1119 		# binary with prefix 0b
1120 		bin1 = 0b11010110
1121 	`);
1122 	assert(doc["hex1"] == 0xDEADBEEF);
1123 	assert(doc["hex2"] == 0xdeadbeef);
1124 	assert(doc["hex3"] == 0xdead_beef);
1125 	assert(doc["oct1"] == 342391);
1126 	assert(doc["oct2"] == 493);
1127 	assert(doc["bin1"] == 0b11010110);
1128 
1129 	assertThrown!TOMLException({ parseTOML(`invalid = 0h111`); }());
1130 
1131 	// -----
1132 	// Float
1133 	// -----
1134 
1135 	doc = parseTOML(`
1136 		# fractional
1137 		flt1 = +1.0
1138 		flt2 = 3.1415
1139 		flt3 = -0.01
1140 
1141 		# exponent
1142 		flt4 = 5e+22
1143 		flt5 = 1e6
1144 		flt6 = -2E-2
1145 
1146 		# both
1147 		flt7 = 6.626e-34
1148 	`);
1149 	assert(doc["flt1"].type == TOML_TYPE.FLOAT);
1150 	assert(doc["flt1"].floating == 1);
1151 	assert(doc["flt2"] == 3.1415);
1152 	assert(doc["flt3"] == -.01);
1153 	assert(doc["flt4"] == 5e+22);
1154 	assert(doc["flt5"] == 1e6);
1155 	assert(doc["flt6"] == -2E-2);
1156 	assert(doc["flt7"] == 6.626e-34);
1157 
1158 	doc = parseTOML(`flt8 = 9_224_617.445_991_228_313`);
1159 	assert(doc["flt8"] == 9_224_617.445_991_228_313);
1160 
1161 	doc = parseTOML(`
1162 		# infinity
1163 		sf1 = inf  # positive infinity
1164 		sf2 = +inf # positive infinity
1165 		sf3 = -inf # negative infinity
1166 
1167 		# not a number
1168 		sf4 = nan  # actual sNaN/qNaN encoding is implementation specific
1169 		sf5 = +nan # same as nan
1170 		sf6 = -nan # valid, actual encoding is implementation specific
1171 	`);
1172 	assert(doc["sf1"] == double.infinity);
1173 	assert(doc["sf2"] == double.infinity);
1174 	assert(doc["sf3"] == -double.infinity);
1175 	assert(doc["sf4"].floating.isNaN());
1176 	assert(doc["sf5"].floating.isNaN());
1177 	assert(doc["sf6"].floating.isNaN());
1178 
1179 	// -------
1180 	// Boolean
1181 	// -------
1182 
1183 	doc = parseTOML(`
1184 		bool1 = true
1185 		bool2 = false
1186 	`);
1187 	assert(doc["bool1"].type == TOML_TYPE.TRUE);
1188 	assert(doc["bool2"].type == TOML_TYPE.FALSE);
1189 	assert(doc["bool1"] == true);
1190 	assert(doc["bool2"] == false);
1191 
1192 	// ----------------
1193 	// Offset Date-Time
1194 	// ----------------
1195 
1196 	doc = parseTOML(`
1197 		odt1 = 1979-05-27T07:32:00Z
1198 		odt2 = 1979-05-27T00:32:00-07:00
1199 		odt3 = 1979-05-27T00:32:00.999999-07:00
1200 	`);
1201 	assert(doc["odt1"].type == TOML_TYPE.OFFSET_DATETIME);
1202 	assert(doc["odt1"].offsetDatetime == SysTime.fromISOExtString("1979-05-27T07:32:00Z"));
1203 	assert(doc["odt2"] == SysTime.fromISOExtString("1979-05-27T00:32:00-07:00"));
1204 	assert(doc["odt3"] == SysTime.fromISOExtString("1979-05-27T00:32:00.999999-07:00"));
1205 
1206 	doc = parseTOML(`odt4 = 1979-05-27 07:32:00Z`);
1207 	assert(doc["odt4"] == SysTime.fromISOExtString("1979-05-27T07:32:00Z"));
1208 
1209 	// ---------------
1210 	// Local Date-Time
1211 	// ---------------
1212 
1213 	doc = parseTOML(`
1214 		ldt1 = 1979-05-27T07:32:00
1215 		ldt2 = 1979-05-27T00:32:00.999999
1216 	`);
1217 	assert(doc["ldt1"].type == TOML_TYPE.LOCAL_DATETIME);
1218 	assert(doc["ldt1"].localDatetime == DateTime.fromISOExtString("1979-05-27T07:32:00"));
1219 	assert(doc["ldt2"] == DateTime.fromISOExtString("1979-05-27T00:32:00.999999"));
1220 
1221 	// ----------
1222 	// Local Date
1223 	// ----------
1224 
1225 	doc = parseTOML(`
1226 		ld1 = 1979-05-27
1227 	`);
1228 	assert(doc["ld1"].type == TOML_TYPE.LOCAL_DATE);
1229 	assert(doc["ld1"].localDate == Date.fromISOExtString("1979-05-27"));
1230 
1231 	// ----------
1232 	// Local Time
1233 	// ----------
1234 
1235 	doc = parseTOML(`
1236 		lt1 = 07:32:00
1237 		lt2 = 00:32:00.999999
1238 	`);
1239 	assert(doc["lt1"].type == TOML_TYPE.LOCAL_TIME);
1240 	assert(doc["lt1"].localTime == TimeOfDay.fromISOExtString("07:32:00"));
1241 	assert(doc["lt2"] == TimeOfDay.fromISOExtString("00:32:00.999999"));
1242 	assert(doc["lt2"].localTime.fracSecs.total!"msecs" == 999999);
1243 
1244 	// -----
1245 	// Array
1246 	// -----
1247 
1248 	doc = parseTOML(`
1249 		arr1 = [ 1, 2, 3 ]
1250 		arr2 = [ "red", "yellow", "green" ]
1251 		arr3 = [ [ 1, 2 ], [3, 4, 5] ]
1252 		arr4 = [ "all", 'strings', """are the same""", '''type''']
1253 		arr5 = [ [ 1, 2 ], ["a", "b", "c"] ]
1254 	`);
1255 	assert(doc["arr1"].type == TOML_TYPE.ARRAY);
1256 	assert(doc["arr1"].array == [TOMLValue(1), TOMLValue(2), TOMLValue(3)]);
1257 	assert(doc["arr2"] == ["red", "yellow", "green"]);
1258 	assert(doc["arr3"] == [[1, 2], [3, 4, 5]]);
1259 	assert(doc["arr4"] == ["all", "strings", "are the same", "type"]);
1260 	assert(doc["arr5"] == [TOMLValue([1, 2]), TOMLValue(["a", "b", "c"])]);
1261 
1262 	assertThrown!TOMLException({ parseTOML(`arr6 = [ 1, 2.0 ]`); }());
1263 
1264 	doc = parseTOML(`
1265 		arr7 = [
1266 		  1, 2, 3
1267 		]
1268 
1269 		arr8 = [
1270 		  1,
1271 		  2, # this is ok
1272 		]
1273 	`);
1274 	assert(doc["arr7"] == [1, 2, 3]);
1275 	assert(doc["arr8"] == [1, 2]);
1276 
1277 	// -----
1278 	// Table
1279 	// -----
1280 
1281 	doc = parseTOML(`
1282 		[table-1]
1283 		key1 = "some string"
1284 		key2 = 123
1285 		
1286 		[table-2]
1287 		key1 = "another string"
1288 		key2 = 456
1289 	`);
1290 	assert(doc["table-1"].type == TOML_TYPE.TABLE);
1291 	assert(doc["table-1"] == ["key1": TOMLValue("some string"), "key2": TOMLValue(123)]);
1292 	assert(doc["table-2"] == ["key1": TOMLValue("another string"), "key2": TOMLValue(456)]);
1293 
1294 	doc = parseTOML(`
1295 		[dog."tater.man"]
1296 		type.name = "pug"
1297 	`);
1298 	assert(doc["dog"]["tater.man"]["type"]["name"] == "pug");
1299 
1300 	doc = parseTOML(`
1301 		[a.b.c]            # this is best practice
1302 		[ d.e.f ]          # same as [d.e.f]
1303 		[ g .  h  . i ]    # same as [g.h.i]
1304 		[ j . "ʞ" . 'l' ]  # same as [j."ʞ".'l']
1305 	`);
1306 	assert(doc["a"]["b"]["c"].type == TOML_TYPE.TABLE);
1307 	assert(doc["d"]["e"]["f"].type == TOML_TYPE.TABLE);
1308 	assert(doc["g"]["h"]["i"].type == TOML_TYPE.TABLE);
1309 	assert(doc["j"]["ʞ"]["l"].type == TOML_TYPE.TABLE);
1310 
1311 	doc = parseTOML(`
1312 		# [x] you
1313 		# [x.y] don't
1314 		# [x.y.z] need these
1315 		[x.y.z.w] # for this to work
1316 	`);
1317 	assert(doc["x"]["y"]["z"]["w"].type == TOML_TYPE.TABLE);
1318 
1319 	doc = parseTOML(`
1320 		[a.b]
1321 		c = 1
1322 		
1323 		[a]
1324 		d = 2
1325 	`);
1326 	assert(doc["a"]["b"]["c"] == 1);
1327 	assert(doc["a"]["d"] == 2);
1328 
1329 	assertThrown!TOMLException({
1330 		parseTOML(`
1331 			# DO NOT DO THIS
1332 				
1333 			[a]
1334 			b = 1
1335 			
1336 			[a]
1337 			c = 2
1338 		`);
1339 	}());
1340 
1341 	assertThrown!TOMLException({
1342 		parseTOML(`
1343 			# DO NOT DO THIS EITHER
1344 
1345 			[a]
1346 			b = 1
1347 
1348 			[a.b]
1349 			c = 2
1350 		`);
1351 	}());
1352 
1353 	assertThrown!TOMLException({ parseTOML(`[]`); }());
1354 	assertThrown!TOMLException({ parseTOML(`[a.]`); }());
1355 	assertThrown!TOMLException({ parseTOML(`[a..b]`); }());
1356 	assertThrown!TOMLException({ parseTOML(`[.b]`); }());
1357 	assertThrown!TOMLException({ parseTOML(`[.]`); }());
1358 
1359 	// ------------
1360 	// Inline Table
1361 	// ------------
1362 
1363 	doc = parseTOML(`
1364 		name = { first = "Tom", last = "Preston-Werner" }
1365 		point = { x = 1, y = 2 }
1366 		animal = { type.name = "pug" }
1367 	`);
1368 	assert(doc["name"]["first"] == "Tom");
1369 	assert(doc["name"]["last"] == "Preston-Werner");
1370 	assert(doc["point"] == ["x": 1, "y": 2]);
1371 	assert(doc["animal"]["type"]["name"] == "pug");
1372 
1373 	// ---------------
1374 	// Array of Tables
1375 	// ---------------
1376 	
1377 	doc = parseTOML(`
1378 		[[products]]
1379 		name = "Hammer"
1380 		sku = 738594937
1381 		
1382 		[[products]]
1383 		
1384 		[[products]]
1385 		name = "Nail"
1386 		sku = 284758393
1387 		color = "gray"
1388 	`);
1389 	assert(doc["products"].type == TOML_TYPE.ARRAY);
1390 	assert(doc["products"].array.length == 3);
1391 	assert(doc["products"][0] == ["name": TOMLValue("Hammer"), "sku": TOMLValue(738594937)]);
1392 	assert(doc["products"][1] == (TOMLValue[string]).init);
1393 	assert(doc["products"][2] == ["name": TOMLValue("Nail"), "sku": TOMLValue(284758393), "color": TOMLValue("gray")]);
1394 
1395 	// nested
1396 	doc = parseTOML(`
1397 		[[fruit]]
1398 		  name = "apple"
1399 
1400 		  [fruit.physical]
1401 		    color = "red"
1402 		    shape = "round"
1403 
1404 		  [[fruit.variety]]
1405 		    name = "red delicious"
1406 
1407 		  [[fruit.variety]]
1408 		    name = "granny smith"
1409 
1410 		[[fruit]]
1411 		  name = "banana"
1412 
1413 		  [[fruit.variety]]
1414 		    name = "plantain"
1415 	`);
1416 	assert(doc["fruit"].type == TOML_TYPE.ARRAY);
1417 	assert(doc["fruit"].array.length == 2);
1418 	assert(doc["fruit"][0]["name"] == "apple");
1419 	assert(doc["fruit"][0]["physical"] == ["color": "red", "shape": "round"]);
1420 	assert(doc["fruit"][0]["variety"][0] == ["name": "red delicious"]);
1421 	assert(doc["fruit"][0]["variety"][1]["name"] == "granny smith");
1422 	assert(doc["fruit"][1] == ["name": TOMLValue("banana"), "variety": TOMLValue([["name": "plantain"]])]);
1423 
1424 	assertThrown!TOMLException({
1425 		parseTOML(`
1426 			# INVALID TOML DOC
1427 			[[fruit]]
1428 			  name = "apple"
1429 
1430 			  [[fruit.variety]]
1431 			    name = "red delicious"
1432 
1433 			  # This table conflicts with the previous table
1434 			  [fruit.variety]
1435 			    name = "granny smith"
1436 		`);
1437 	}());
1438 
1439 	doc = parseTOML(`
1440 		points = [ { x = 1, y = 2, z = 3 },
1441 			{ x = 7, y = 8, z = 9 },
1442 			{ x = 2, y = 4, z = 8 } ]
1443 	`);
1444 	assert(doc["points"].array.length == 3);
1445 	assert(doc["points"][0] == ["x": 1, "y": 2, "z": 3]);
1446 	assert(doc["points"][1] == ["x": 7, "y": 8, "z": 9]);
1447 	assert(doc["points"][2] == ["x": 2, "y": 4, "z": 8]);
1448 
1449 
1450 
1451 	// additional tests for code coverage
1452 
1453 	assert(TOMLValue(42) == 42.0);
1454 	assert(TOMLValue(42) != "42");
1455 	assert(TOMLValue("42") != 42);
1456 
1457 	try {
1458 		parseTOML(`
1459 			
1460 			error = @		
1461 		`);
1462 	} catch(TOMLParserException e) {
1463 		assert(e.position.line == 3); // start from line 1
1464 		assert(e.position.column == 9 + 3); // 3 tabs
1465 	}
1466 
1467 	assertThrown!TOMLException({ parseTOML(`error = "unterminated`); }());
1468 	assertThrown!TOMLException({ parseTOML(`error = 'unterminated`); }());
1469 	assertThrown!TOMLException({ parseTOML(`error = "\ "`); }());
1470 
1471 	assertThrown!TOMLException({ parseTOML(`error = truè`); }());
1472 	assertThrown!TOMLException({ parseTOML(`error = falsè`); }());
1473 
1474 	assertThrown!TOMLException({ parseTOML(`[error`); }());
1475 
1476 	doc = parseTOML(`test = "\\\"\b\t\n\f\r\u0040\U00000040"`);
1477 	assert(doc["test"] == "\\\"\b\t\n\f\r@@");
1478 
1479 	doc = parseTOML(`test = """quoted "string"!"""`);
1480 	assert(doc["test"] == "quoted \"string\"!");
1481 
1482 	// options
1483 
1484 	assert(parseTOML(`raw = this is unquoted`, TOMLOptions.unquotedStrings)["raw"] == "this is unquoted");
1485 
1486 	// document
1487 
1488 	TOMLValue value = TOMLValue(["test": 44]);
1489 	doc = TOMLDocument(value);
1490 
1491 	// opEquals
1492 
1493 	assert(TOMLValue(true) == TOMLValue(true));
1494 	assert(TOMLValue("string") == TOMLValue("string"));
1495 	assert(TOMLValue(0) == TOMLValue(0));
1496 	assert(TOMLValue(.0) == TOMLValue(.0));
1497 	assert(TOMLValue(SysTime.fromISOExtString("1979-05-27T00:32:00-07:00")) == TOMLValue(SysTime.fromISOExtString("1979-05-27T00:32:00-07:00")));
1498 	assert(TOMLValue(DateTime.fromISOExtString("1979-05-27T07:32:00")) == TOMLValue(DateTime.fromISOExtString("1979-05-27T07:32:00")));
1499 	assert(TOMLValue(Date.fromISOExtString("1979-05-27")) == TOMLValue(Date.fromISOExtString("1979-05-27")));
1500 	assert(TOMLValue(TimeOfDay.fromISOExtString("07:32:00")) == TOMLValue(TimeOfDay.fromISOExtString("07:32:00")));
1501 	assert(TOMLValue([1, 2, 3]) == TOMLValue([1, 2, 3]));
1502 	assert(TOMLValue(["a": 0, "b": 1]) == TOMLValue(["a": 0, "b": 1]));
1503 
1504 	// toString()
1505 
1506 	assert(TOMLDocument(["test": TOMLValue(0)]).toString() == "test = 0" ~ newline);
1507 
1508 	assert(TOMLValue(true).toString() == "true");
1509 	assert(TOMLValue("string").toString() == "\"string\"");
1510 	assert(TOMLValue("\"quoted \\ \b \f \r\n \t string\"").toString() == "\"\\\"quoted \\\\ \\b \\f \\r\\n \\t string\\\"\"");
1511 	assert(TOMLValue(42).toString() == "42");
1512 	assert(TOMLValue(99.44).toString() == "99.44");
1513 	assert(TOMLValue(.0).toString() == "0.0");
1514 	assert(TOMLValue(1e100).toString() == "1e+100");
1515 	assert(TOMLValue(SysTime.fromISOExtString("1979-05-27T00:32:00-07:00")).toString() == "1979-05-27T00:32:00-07:00");
1516 	assert(TOMLValue(DateTime.fromISOExtString("1979-05-27T07:32:00")).toString() == "1979-05-27T07:32:00");
1517 	assert(TOMLValue(Date.fromISOExtString("1979-05-27")).toString() == "1979-05-27");
1518 	assert(TOMLValue(TimeOfDay.fromISOExtString("07:32:00.999999")).toString() == "07:32:00.999999");
1519 	assert(TOMLValue([1, 2, 3]).toString() == "[1, 2, 3]");
1520 	immutable table = TOMLValue(["a": 0, "b": 1]).toString();
1521 	assert(table == "{ a = 0, b = 1 }" || table == "{ b = 1, a = 0 }");
1522 
1523 	foreach(key, value; TOMLValue(["0": 0, "1": 1])) {
1524 		assert(value == key.to!int);
1525 	}
1526 
1527 	value = 42;
1528 	assert(value.type == TOML_TYPE.INTEGER);
1529 	assert(value == 42);
1530 	value = TOMLValue("42");
1531 	assert(value.type == TOML_TYPE.STRING);
1532 	assert(value == "42");
1533 
1534 }
1535 
1536 /**
1537  * Exception thrown on generic TOML errors.
1538  */
1539 class TOMLException : Exception {
1540 
1541 	public this(string message, string file=__FILE__, size_t line=__LINE__) {
1542 		super(message, file, line);
1543 	}
1544 
1545 }
1546 
1547 /**
1548  * Exception thrown during the parsing of TOML document.
1549  */
1550 class TOMLParserException : TOMLException {
1551 
1552 	private Tuple!(size_t, "line", size_t, "column") _position;
1553 
1554 	public this(string message, size_t line, size_t column, string file=__FILE__, size_t _line=__LINE__) {
1555 		super(message ~ " (" ~ to!string(line) ~ ":" ~ to!string(column) ~ ")", file, _line);
1556 		this._position.line = line;
1557 		this._position.column = column;
1558 	}
1559 
1560 	/**
1561 	 * Gets the position (line and column) where the parsing expection
1562 	 * has occured.
1563 	 */
1564 	public pure nothrow @property @safe @nogc auto position() {
1565 		return this._position;
1566 	}
1567 
1568 }