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