1 module sdlite.lexer;
2 
3 import sdlite.ast : SDLValue;
4 import sdlite.internal : MultiAppender;
5 
6 import std.algorithm.comparison : among;
7 import std.algorithm.mutation : move, swap;
8 import std.range;
9 import std.datetime;
10 import std.uni : isAlpha, isWhite;
11 import std.utf : byCodeUnit, decodeFront;
12 import core.time : Duration, days, hours, minutes, seconds, msecs, hnsecs;
13 
14 
15 struct Token(R) {
16 	alias SourceRange = R;
17 
18 	TokenType type;
19 	Location location;
20 	Take!R whitespacePrefix;
21 	Take!R text;
22 }
23 
24 enum TokenType {
25 	invalid,
26 	eof,
27 	eol,
28 	assign,
29 	namespace,
30 	blockOpen,
31 	blockClose,
32 	semicolon,
33 	comment,
34 	identifier,
35 	null_,
36 	text,
37 	binary,
38 	number,
39 	boolean,
40 	dateTime,
41 	date,
42 	duration
43 }
44 
45 struct Location {
46 	/// Name of the source file
47 	string file;
48 	/// Line within the file (Unix/Windows/Mac line endings are recognized)
49 	size_t line;
50 	/// Byte offset from the start of the line
51 	size_t column;
52 	/// Byte offset from the start of the input string
53 	size_t offset;
54 }
55 
56 
57 /** Returns a range of `SDLToken`s by lexing the given SDLang input.
58 */
59 auto lexSDLang(const(char)[] input, string filename = "")
60 {
61 	return lexSDLang(input.byCodeUnit, filename);
62 }
63 /// ditto
64 auto lexSDLang(R)(R input, string filename = "")
65 	if (isForwardRange!R && is(immutable(ElementType!R) == immutable(char)))
66 {
67 	return SDLangLexer!R(input, filename);
68 }
69 
70 
71 package SDLValue parseValue(R)(ref Token!R t,
72 	ref MultiAppender!(immutable(char)) char_appender,
73 	ref MultiAppender!(immutable(ubyte)) byte_appender)
74 {
75 	import std.algorithm.comparison : min, max;
76 	import std.algorithm.iteration : splitter;
77 	import std.algorithm.searching : endsWith, findSplit;
78 	import std.conv : parse, to;
79 	import std.exception : assumeUnique;
80 	import std.format : formattedRead;
81 	import std.typecons : Rebindable;
82 	import std.uni : icmp;
83 
84 	final switch (t.type) {
85 		case TokenType.invalid:
86 		case TokenType.eof:
87 		case TokenType.eol:
88 		case TokenType.assign:
89 		case TokenType.namespace:
90 		case TokenType.blockOpen:
91 		case TokenType.blockClose:
92 		case TokenType.semicolon:
93 		case TokenType.comment:
94 		case TokenType.identifier:
95 			 return SDLValue.null_;
96 		case TokenType.null_:
97 			 return SDLValue.null_;
98 		case TokenType.text:
99 			assert(!t.text.empty);
100 			if (t.text.front == '`') {
101 				auto txt = t.text
102 					.save
103 					.dropOne
104 					.take(t.text.length - 2);
105 				foreach (ch; txt) char_appender.put(ch);
106 				return SDLValue.text(char_appender.extractArray);
107 			} else {
108 				assert(t.text.front == '"');
109 				t.parseTextValue(char_appender);
110 				return SDLValue.text(char_appender.extractArray);
111 			}
112 		case TokenType.binary:
113 			t.parseBinaryValue(byte_appender);
114 			return SDLValue.binary(byte_appender.extractArray);
115 		case TokenType.number:
116 			auto numparts = t.text.save.findSplit(".");
117 			if (numparts[1].empty) { // integer or integer-like float
118 				auto num = parse!long(numparts[0]);
119 				if (numparts[0].empty)
120 					return SDLValue.int_(cast(int)num.min(int.max).max(int.min));
121 
122 				switch (numparts[0].front) {
123 					default: assert(false);
124 					case 'l', 'L': return SDLValue.long_(num);
125 					case 'd', 'D': return SDLValue.double_(num);
126 					case 'f', 'F': return SDLValue.float_(num);
127 				}
128 			}
129 
130 			auto r = t.text.save;
131 
132 			if (numparts[2].length >= 2) {
133 				if (numparts[2].save.tail(2).icmp("bd") == 0)
134 					return SDLValue.null_; // decimal not yet supported
135 				if (numparts[2].save.retro.front.among!('f', 'F'))
136 					return SDLValue.float_(r.parse!float);
137 			}
138 			return SDLValue.double_(r.parse!double);
139 		case TokenType.boolean:
140 			switch (t.text.front) {
141 				default: assert(false);
142 				case 't': return SDLValue.bool_(true);
143 				case 'f': return SDLValue.bool_(false);
144 				case 'o':
145 					auto txt = t.text.save.dropOne;
146 					return SDLValue.bool_(txt.front == 'n');
147 			}
148 		case TokenType.date:
149 			int y, m, d;
150 			t.text.save.formattedRead("%d/%d/%d", y, m, d);
151 			return SDLValue.date(Date(y, m, d));
152 		case TokenType.duration:
153 			auto parts = t.text.save.splitter(":");
154 			int d, h, m, s;
155 			if (parts.front.save.endsWith("d")) {
156 				d = parts.front.dropBackOne.to!int();
157 				parts.popFront();
158 			}
159 			h = parts.front.to!int();
160 			parts.popFront();
161 			m = parts.front.to!int();
162 			parts.popFront();
163 			auto sec = parts.front.findSplit(".");
164 			s = sec[0].to!int;
165 			Duration fracsec = Duration.zero;
166 			if (!sec[1].empty) {
167 				auto l0 = sec[2].length;
168 				long fs = sec[2].parse!long();
169 				fracsec = (fs * (10 ^^ (7 - l0))).hnsecs;
170 			}
171 			return SDLValue.duration(d.days + h.hours + m.minutes + s.seconds + fracsec);
172 		case TokenType.dateTime:
173 			int y, m, d, hh, mm, ss;
174 			auto txt = t.text.save;
175 			txt.formattedRead("%d/%d/%d %d:%d", y, m, d, hh, mm);
176 			if (!txt.empty && txt.front == ':') {
177 				txt.popFront();
178 				ss = txt.parse!int();
179 			}
180 			auto dt = DateTime(y, m, d, hh, mm, ss);
181 			Rebindable!(immutable(TimeZone)) tz;
182 			Duration fracsec = Duration.zero;
183 
184 			if (!txt.empty && txt.front == '.') {
185 				txt.popFront();
186 				auto l0 = txt.length;
187 				long fs = txt.parse!long();
188 				fracsec = (fs * (10 ^^ (7 - l0))).hnsecs;
189 			}
190 
191 			if (!txt.empty) {
192 				txt.popFront();
193 				char[3] tzt;
194 				txt.formattedRead("%c%c%c", tzt[0], tzt[1], tzt[2]);
195 				if (!txt.empty) {
196 					int mul = txt.front == '-' ? -1 : 1;
197 					txt.popFront();
198 					int dh = txt.parse!int();
199 					int dm = 0;
200 					if (!txt.empty) {
201 						txt.formattedRead(":%d", dm);
202 					}
203 					tz = new immutable SimpleTimeZone((mul*dh).hours + (mul*dm).minutes);
204 				} else if (tzt == "UTC" || tzt == "GMT") {
205 					tz = UTC();
206 				} else {
207 					version (Windows) tz = WindowsTimeZone.getTimeZone(tzt[].idup);
208 					else tz = PosixTimeZone.getTimeZone(tzt[].idup);
209 				}
210 			} else tz = LocalTime();
211 
212 			return SDLValue.dateTime(SysTime(dt, fracsec, tz));
213 	}
214 }
215 
216 package void parseTextValue(R, DR)(ref Token!R t, ref DR dst)
217 {
218 	import std.algorithm.mutation : copy;
219 
220 	assert(t.type == TokenType.text);
221 	assert(!t.text.empty);
222 
223 	auto content = t.text.save.dropOne().take(t.text.length - 2);
224 
225 	if (t.text.front == '`') { // WYSIWYG string
226 		foreach (char ch; content)
227 			dst.put(ch);
228 		return;
229 	}
230 
231 	assert(t.text.front == '"');
232 
233 	static void skipWhitespace(R)(ref R r)
234 	{
235 		while (!r.empty && r.front.among!(' ', '\t'))
236 			r.popFront();
237 	}
238 
239 	while (content.length) {
240 		char ch = content.front;
241 		content.popFront();
242 
243 		if (ch != '\\') dst.put(ch);
244 		else {
245 			assert(!content.empty);
246 			ch = content.front;
247 			content.popFront();
248 
249 			switch (ch) {
250 				default: assert(false);
251 				case '\r':
252 					if (!content.empty && content.front == '\n')
253 						content.popFront();
254 					skipWhitespace(content);
255 					break;
256 				case '\n': skipWhitespace(content); break;
257 				case 'r': dst.put('\r'); break;
258 				case 'n': dst.put('\n'); break;
259 				case 't': dst.put('\t'); break;
260 				case '"': dst.put('"'); break;
261 				case '\\': dst.put('\\'); break;
262 			}
263 		}
264 	}
265 }
266 
267 package void parseBinaryValue(R, DR)(ref Token!R t, ref DR dst)
268 {
269 	import std.base64 : Base64;
270 
271 	assert(!t.text.empty);
272 	assert(t.text.front == '[');
273 
274 	auto content = t.text.save.dropOne.take(t.text.length - 2);
275 	char[4] buf;
276 
277 	while (!content.empty) {
278 		foreach (i; 0 .. 4) {
279 			while (content.front.among!(' ', '\t', '\r', '\n'))
280 				content.popFront();
281 			buf[i] = content.front;
282 			content.popFront();
283 		}
284 
285 		ubyte[3] bytes;
286 		dst.put(Base64.decode(buf[], bytes[]));
287 	}
288 }
289 
290 private struct SDLangLexer(R)
291 	if (isForwardRange!R && is(immutable(ElementType!R) == immutable(char)))
292 {
293 	private {
294 		R m_input;
295 		Location m_location;
296 		Token!R m_token;
297 		bool m_empty;
298 	}
299 
300 	/** Initializes a lexer for the given input SDL document.
301 
302 		The document must be given in the form of a UTF-8 encoded text that is
303 		stored as a `ubyte` forward range.
304 	*/
305 	this(R input, string filename)
306 	{
307 		m_input = input.move;
308 		m_location.file = filename;
309 
310 		readNextToken();
311 	}
312 
313 	@property bool empty() const { return m_empty; }
314 
315 	ref inout(Token!R) front()
316 	inout {
317 		return m_token;
318 	}
319 
320 	SDLangLexer save()
321 	{
322 		SDLangLexer ret;
323 		ret.m_input = m_input.save;
324 		ret.m_location = m_location;
325 		ret.m_token = m_token;
326 		ret.m_empty = m_empty;
327 		return ret;
328 	}
329 
330 	void popFront()
331 	{
332 		assert(!empty);
333 		if (m_token.type == TokenType.eof) m_empty = true;
334 		else readNextToken();
335 	}
336 
337 	private void readNextToken()
338 	{
339 		import std.algorithm.comparison : equal;
340 
341 		m_token.whitespacePrefix = skipWhitespace();
342 		m_token.location = m_location;
343 
344 		if (m_input.empty) {
345 			m_token.type = TokenType.eof;
346 			m_token.text = m_input.take(0);
347 			return;
348 		}
349 
350 		auto tstart = m_input.save;
351 		m_token.type = skipToken();
352 		m_token.text = tstart.take(m_location.offset - m_token.location.offset);
353 
354 		// keywords are initially parsed as identifiers
355 		if (m_token.type == TokenType.identifier
356 			&& m_token.text.front.among!('o', 't', 'f', 'n'))
357 		{
358 			if (m_token.text.equal("on") || m_token.text.equal("off") ||
359 				m_token.text.equal("true") || m_token.text.equal("false"))
360 			{
361 				m_token.type = TokenType.boolean;
362 			} else if (m_token.text.equal("null")) {
363 				m_token.type = TokenType.null_;
364 			}
365 		}
366 	}
367 
368 	private TokenType skipToken()
369 	{
370 		switch (m_input.front) {
371 			case '\r':
372 				skipChar!true();
373 				if (!m_input.empty && m_input.front == '\n')
374 					skipChar!false();
375 				return TokenType.eol;
376 			case '\n':
377 				skipChar!true();
378 				return TokenType.eol;
379 			case '/': // C/C++ style comment
380 				skipChar!false();
381 				if (m_input.empty || !m_input.front.among!('/', '*')) {
382 					return TokenType.invalid;
383 				}
384 				if (m_input.front == '/') {
385 					skipChar!false();
386 					skipLine();
387 
388 					return TokenType.comment;
389 				}
390 
391 				skipChar!false();
392 
393 				while (true) {
394 					while (!m_input.empty && m_input.front != '*')
395 						skipChar!true();
396 
397 					if (!m_input.empty) skipChar!false();
398 
399 					if (m_input.empty) {
400 						return TokenType.invalid;
401 					}
402 
403 					if (m_input.front == '/') {
404 						skipChar!false();
405 						return TokenType.comment;
406 					}
407 				}
408 				assert(false);
409 			case '-': // LUA style comment or negative number
410 				skipChar!false();
411 
412 				if (m_input.empty) return TokenType.invalid;
413 
414 				auto ch = m_input.front;
415 				if (ch >= '0' && ch <= '9')
416 					return skipNumericToken();
417 
418 				if (ch != '-') return TokenType.invalid;
419 
420 				skipChar!false();
421 				skipLine();
422 
423 				return TokenType.comment;
424 			case '#': // shell style comment
425 				skipChar!false();
426 				skipLine();
427 				return TokenType.comment;
428 			case '"': // normal string
429 				skipChar!false();
430 
431 				outerstr: while (!m_input.empty) {
432 					char ch = m_input.front;
433 					if (ch.among!('\r', '\n')) break;
434 
435 					skipChar!false();
436 
437 					if (ch == '"') {
438 						return TokenType.text;
439 					} else if (ch == '\\') {
440 						ch = m_input.front;
441 						skipChar!false();
442 						switch (ch) {
443 							default: break outerstr;
444 							case '"', '\\', 'n', 'r', 't': break;
445 							case '\n', '\r':
446 								skipChar!true();
447 								skipWhitespace();
448 								break;
449 						}
450 					}
451 				}
452 
453 				return TokenType.invalid;
454 			case '`': // WYSIWYG string
455 				skipChar!false();
456 
457 				while (!m_input.empty) {
458 					if (m_input.front == '`') {
459 						skipChar!false();
460 						return TokenType.text;
461 					}
462 
463 					skipChar!true();
464 				}
465 
466 				return TokenType.invalid;
467 			case '[': // base64 data
468 				import std.array : appender;
469 
470 				skipChar!false();
471 
472 
473 				uint chunklen = 0;
474 
475 				while (!m_input.empty) {
476 					auto ch = m_input.front;
477 					switch (ch) {
478 						case ']':
479 							skipChar!false();
480 							if (chunklen != 0) { // content length must be a multiple of 4
481 								return TokenType.invalid;
482 							}
483 							return TokenType.binary;
484 						case '0': .. case '9':
485 						case 'A': .. case 'Z':
486 						case 'a': .. case 'z':
487 						case '+', '/', '=':
488 							if (++chunklen == 4)
489 								chunklen = 0;
490 							skipChar!false();
491 							break;
492 						case ' ', '\t': skipChar!false(); break;
493 						case '\r', '\n': skipChar!true(); break;
494 						default: return TokenType.invalid;
495 					}
496 
497 				}
498 
499 				return TokenType.invalid;
500 			case '{': skipChar!false(); return TokenType.blockOpen;
501 			case '}': skipChar!false(); return TokenType.blockClose;
502 			case ';': skipChar!false(); return TokenType.semicolon;
503 			case '=': skipChar!false(); return TokenType.assign;
504 			case ':': skipChar!false(); return TokenType.namespace;
505 			case '0': .. case '9': // number or date/time
506 				return skipNumericToken();
507 			default: // identifier
508 				const chf = m_input.front;
509 				switch (chf) {
510 					case '0': .. case '9':
511 					case 'A': .. case 'Z':
512 					case 'a': .. case 'z':
513 					case '_':
514 						skipChar!false();
515 						break;
516 					default:
517 						size_t n;
518 						auto dch = m_input.decodeFront(n);
519 						m_location.offset += n;
520 						m_location.column += n;
521 						if (!dch.isAlpha && dch != '_')
522 							return TokenType.invalid;
523 						break;
524 				}
525 
526 				outer: while (!m_input.empty) {
527 					const ch = m_input.front;
528 					switch (ch) {
529 						case '0': .. case '9':
530 						case 'A': .. case 'Z':
531 						case 'a': .. case 'z':
532 						case '_', '-', '.', '$':
533 							skipChar!false();
534 							break;
535 						default:
536 							// all eglible ASCII characters are handled above
537 							if (!(ch & 0x80)) break outer;
538 
539 							// test if this is a Unicode alphabectical character
540 							auto inp = m_input.save;
541 							size_t n;
542 							dchar dch = m_input.decodeFront(n);
543 							if (!isAlpha(dch)) {
544 								swap(inp, m_input);
545 								break outer;
546 							}
547 							m_location.offset += n;
548 							m_location.column += n;
549 							break;
550 
551 					}
552 				}
553 
554 				return TokenType.identifier;
555 		}
556 	}
557 
558 	private TokenType skipNumericToken()
559 	{
560 		assert(m_input.front >= '0' && m_input.front <= '9');
561 		skipChar!false();
562 
563 		while (!m_input.empty && m_input.front >= '0' && m_input.front <= '9')
564 			skipChar!false();
565 
566 		if (m_input.empty) // unqualified integer
567 			return TokenType.number;
568 
569 		auto ch = m_input.front;
570 		switch (ch) { // unqualified integer
571 			default:
572 				return TokenType.number;
573 			case ':': // time span
574 				if (!skipDuration(No.includeFirstNumber)) {
575 					return TokenType.invalid;
576 				}
577 				return TokenType.duration;
578 			case 'D': // double with no fractional part
579 				skipChar!false();
580 				return TokenType.number;
581 			case 'f', 'F': // float with no fractional part
582 				skipChar!false();
583 				return TokenType.number;
584 			case 'd': // time span with days or double value
585 				skipChar!false();
586 				if (m_input.empty || m_input.front != ':') {
587 					return TokenType.number;
588 				}
589 
590 				skipChar!false();
591 
592 				if (!skipDuration(Yes.includeFirstNumber)) {
593 					return TokenType.invalid;
594 				}
595 				return TokenType.duration;
596 			case '/': // date
597 				if (!skipDate(No.includeFirstNumber)) {
598 					return TokenType.invalid;
599 				}
600 				if (m_input.empty || m_input.front != ' ') {
601 					return TokenType.date;
602 				}
603 
604 				auto input_saved = m_input.save;
605 				auto loc_saved = m_location;
606 
607 				skipChar!false();
608 
609 				if (!skipTimeOfDay()) {
610 					swap(m_input, input_saved);
611 					swap(m_location, loc_saved);
612 					return TokenType.date;
613 				}
614 
615 				if (!m_input.empty && m_input.front == '-') {
616 					skipChar!false();
617 					if (!skipTimeZone()) {
618 						return TokenType.invalid;
619 					}
620 				}
621 
622 				return TokenType.dateTime;
623 			case '.': // floating point
624 				skipChar!false();
625 				if (m_input.front < '0' || m_input.front > '9') {
626 					return TokenType.invalid;
627 				}
628 
629 				while (!m_input.empty && m_input.front >= '0' && m_input.front <= '9')
630 					skipChar!false();
631 
632 				if (m_input.empty || m_input.front.among!('f', 'F', 'd', 'D')) { // IEEE floating-point
633 					if (!m_input.empty) skipChar!false();
634 					return TokenType.number;
635 				}
636 
637 				if (m_input.front.among!('b', 'B')) { // decimal
638 					skipChar!false();
639 					if (!m_input.front.among!('d', 'D')) { // FIXME: only "bd" or "BD" should be allowed, not "bD"
640 						return TokenType.invalid;
641 					}
642 
643 					skipChar!false();
644 					return TokenType.number;
645 				}
646 
647 				return TokenType.number;
648 			case 'l', 'L': // long integer
649 				skipChar!false();
650 				return TokenType.number;
651 		}
652 	}
653 
654 	private Take!R skipWhitespace()
655 	{
656 		size_t n = 0;
657 		auto ret = m_input.save;
658 		while (!m_input.empty && m_input.front.among!(' ', '\t')) {
659 			skipChar!false();
660 			n++;
661 		}
662 		return ret.take(n);
663 	}
664 
665 	private bool skipOver(string s)
666 	{
667 		while (!m_input.empty && s.length > 0) {
668 			if (m_input.front != s[0]) return false;
669 			s = s[1 .. $];
670 			m_location.offset++;
671 			m_location.column++;
672 			m_input.popFront();
673 		}
674 		return s.length == 0;
675 	}
676 
677 	private void skipLine()
678 	{
679 		while (!m_input.empty && !m_input.front.among!('\r', '\n'))
680 			skipChar!false();
681 		if (!m_input.empty) skipChar!true();
682 	}
683 
684 	private void skipChar(bool could_be_eol)()
685 	{
686 		static if (could_be_eol) {
687 			auto c = m_input.front;
688 			m_input.popFront();
689 			m_location.offset++;
690 			if (c == '\r') {
691 				m_location.line++;
692 				m_location.column = 0;
693 				if (!m_input.empty && m_input.front == '\n') {
694 					m_input.popFront();
695 					m_location.offset++;
696 				}
697 			} else if (c == '\n') {
698 				m_location.line++;
699 				m_location.column = 0;
700 			} else m_location.column++;
701 		} else {
702 			m_input.popFront();
703 			m_location.offset++;
704 			m_location.column++;
705 		}
706 	}
707 
708 	private bool skipDate(Flag!"includeFirstNumber" include_first_number)
709 	{
710 		if (include_first_number)
711 			if (!skipInteger()) return false;
712 		if (!skipOver("/")) return false;
713 		if (!skipInteger()) return false;
714 		if (!skipOver("/")) return false;
715 		if (!skipInteger()) return false;
716 		return true;
717 	}
718 
719 	private bool skipDuration(Flag!"includeFirstNumber" include_first_number)
720 	{
721 		if (include_first_number)
722 			if (!skipInteger()) return false;
723 		if (!skipOver(":")) return false;
724 		if (!skipInteger()) return false;
725 		if (!skipOver(":")) return false;
726 		if (!skipInteger()) return false;
727 		if (!m_input.empty && m_input.front == '.') {
728 			skipChar!false();
729 			if (!skipInteger()) return false;
730 		}
731 		return true;
732 	}
733 
734 	private bool skipTimeOfDay()
735 	{
736 		if (!skipInteger()) return false;
737 		if (!skipOver(":")) return false;
738 		if (!skipInteger()) return false;
739 		if (!m_input.empty && m_input.front != ':') return true;
740 		skipChar!false();
741 		if (!skipInteger()) return false;
742 		if (!m_input.empty && m_input.front == '.') {
743 			skipChar!false();
744 			if (!skipInteger()) return false;
745 		}
746 		return true;
747 	}
748 
749 	private bool skipTimeZone()
750 	{
751 		foreach (i; 0 .. 3) {
752 			auto ch = m_input.front;
753 			if (ch < 'A' || ch > 'Z') return false;
754 			skipChar!false();
755 		}
756 
757 		if (m_input.empty || !m_input.front.among!('-', '+'))
758 			return true;
759 		skipChar!false();
760 
761 		if (!skipInteger()) return false;
762 
763 		if (m_input.empty || m_input.front != ':')
764 			return true;
765 		skipChar!false();
766 
767 		if (!skipInteger()) return false;
768 
769 		return true;
770 	}
771 
772 	private bool skipInteger()
773 	{
774 		if (m_input.empty) return false;
775 
776 		char ch = m_input.front;
777 		if (ch < '0' || ch > '9') return false;
778 		skipChar!false();
779 
780 		while (!m_input.empty) {
781 			ch = m_input.front;
782 			if (ch < '0' || ch > '9') break;
783 			skipChar!false();
784 		}
785 
786 		return true;
787 	}
788 }
789 
790 unittest { // single token tests
791 	MultiAppender!(immutable(char)) chapp;
792 	MultiAppender!(immutable(ubyte)) btapp;
793 
794 	void test(string sdl, TokenType tp, string txt, SDLValue val = SDLValue.null_, string ws = "", bool multiple = false)
795 	{
796 		auto t = SDLangLexer!(typeof(sdl.byCodeUnit))(sdl.byCodeUnit, "test");
797 		assert(!t.empty);
798 		assert(t.front.type == tp);
799 		assert(t.front.whitespacePrefix.source == ws);
800 		assert(t.front.text.source == txt);
801 		assert(t.front.parseValue(chapp, btapp) == val);
802 		t.popFront();
803 		assert(multiple || t.front.type == TokenType.eof);
804 	}
805 
806 	test("\n", TokenType.eol, "\n");
807 	test("\r", TokenType.eol, "\r");
808 	test("\r\n", TokenType.eol, "\r\n");
809 	test("=", TokenType.assign, "=");
810 	test(":", TokenType.namespace, ":");
811 	test("{", TokenType.blockOpen, "{");
812 	test("}", TokenType.blockClose, "}");
813 	test("// foo", TokenType.comment, "// foo");
814 	test("# foo", TokenType.comment, "# foo");
815 	test("-- foo", TokenType.comment, "-- foo");
816 	test("-- foo\n", TokenType.comment, "-- foo\n");
817 	test("foo", TokenType.identifier, "foo");
818 	test("foo ", TokenType.identifier, "foo");
819 	test("foo$.-_ ", TokenType.identifier, "foo$.-_");
820 	test("föö", TokenType.identifier, "föö");
821 	test("null", TokenType.null_, "null", SDLValue.null_);
822 	test("true", TokenType.boolean, "true", SDLValue.bool_(true));
823 	test("false", TokenType.boolean, "false", SDLValue.bool_(false));
824 	test("on", TokenType.boolean, "on", SDLValue.bool_(true));
825 	test("off", TokenType.boolean, "off", SDLValue.bool_(false));
826 	test("on_", TokenType.identifier, "on_");
827 	test("off_", TokenType.identifier, "off_");
828 	test("true_", TokenType.identifier, "true_");
829 	test("false_", TokenType.identifier, "false_");
830 	test("null_", TokenType.identifier, "null_");
831 	test("-", TokenType.invalid, "-");
832 	test("%", TokenType.invalid, "%");
833 	test("\\", TokenType.invalid, "\\");
834 	//test("\\\n", TokenType.eof, "\\\n");
835 	test("`foo`", TokenType.text, "`foo`", SDLValue.text("foo"));
836 	test("`fo\\\"o`", TokenType.text, "`fo\\\"o`", SDLValue.text("fo\\\"o"));
837 	test(`"foo"`, TokenType.text, `"foo"`, SDLValue.text("foo"));
838 	test(`"f\"oo"`, TokenType.text, `"f\"oo"`, SDLValue.text("f\"oo"));
839 	test("\"f \\\n   oo\"", TokenType.text, "\"f \\\n   oo\"", SDLValue.text("f oo"));
840 	test("[aGVsbG8sIHdvcmxkIQ==]", TokenType.binary, "[aGVsbG8sIHdvcmxkIQ==]", SDLValue.binary(cast(immutable(ubyte)[])"hello, world!"));
841 	test("[aGVsbG8sI \t \n \t HdvcmxkIQ==]", TokenType.binary, "[aGVsbG8sI \t \n \t HdvcmxkIQ==]", SDLValue.binary(cast(immutable(ubyte)[])"hello, world!"));
842 	test("[aGVsbG8sIHdvcmxkIQ]", TokenType.invalid, "[aGVsbG8sIHdvcmxkIQ]");
843 	test("[aGVsbG8sIHdvcmxk$Q==]", TokenType.invalid, "[aGVsbG8sIHdvcmxk", SDLValue.null_, "", true);
844 	test("5", TokenType.number, "5", SDLValue.int_(5));
845 	test("123", TokenType.number, "123", SDLValue.int_(123));
846 	test("-123", TokenType.number, "-123", SDLValue.int_(-123));
847 	test("123l", TokenType.number, "123l", SDLValue.long_(123));
848 	test("123L", TokenType.number, "123L", SDLValue.long_(123));
849 	test("123.123", TokenType.number, "123.123", SDLValue.double_(123.123));
850 	test("123.123f", TokenType.number, "123.123f", SDLValue.float_(123.123));
851 	test("123.123F", TokenType.number, "123.123F", SDLValue.float_(123.123));
852 	test("123.123d", TokenType.number, "123.123d", SDLValue.double_(123.123));
853 	test("123.123D", TokenType.number, "123.123D", SDLValue.double_(123.123));
854 	test("123d", TokenType.number, "123d", SDLValue.double_(123));
855 	test("123D", TokenType.number, "123D", SDLValue.double_(123));
856 	test("1.0", TokenType.number, "1.0", SDLValue.double_(1.0));
857 	test("123.123bd", TokenType.number, "123.123bd"); // TODO
858 	test("123.123BD", TokenType.number, "123.123BD"); // TODO
859 	test("2015/12/06", TokenType.date, "2015/12/06", SDLValue.date(Date(2015, 12, 6)));
860 	test("12:14:34", TokenType.duration, "12:14:34", SDLValue.duration(12.hours + 14.minutes + 34.seconds));
861 	test("12:14:34.123", TokenType.duration, "12:14:34.123", SDLValue.duration(12.hours + 14.minutes + 34.seconds + 123.msecs));
862 	test("2d:12:14:34", TokenType.duration, "2d:12:14:34", SDLValue.duration(2.days + 12.hours + 14.minutes + 34.seconds));
863 	test("2015/12/06 12:00:00.000", TokenType.dateTime, "2015/12/06 12:00:00.000", SDLValue.dateTime(SysTime(DateTime(2015, 12, 6, 12, 0, 0))));
864 	test("2015/12/06 12:00:00.000-UTC", TokenType.dateTime, "2015/12/06 12:00:00.000-UTC", SDLValue.dateTime(SysTime(DateTime(2015, 12, 6, 12, 0, 0), UTC())));
865 	test("2015/12/06 12:00:00-GMT-2:30", TokenType.dateTime, "2015/12/06 12:00:00-GMT-2:30", SDLValue.dateTime(SysTime(DateTime(2015, 12, 6, 12, 0, 0), new immutable SimpleTimeZone(-2.hours - 30.minutes))));
866 	test("2015/12/06 12:00:00-GMT+0:31", TokenType.dateTime, "2015/12/06 12:00:00-GMT+0:31", SDLValue.dateTime(SysTime(DateTime(2015, 12, 6, 12, 0, 0), new immutable SimpleTimeZone(31.minutes))));
867 	test("2015/12/06 ", TokenType.date, "2015/12/06", SDLValue.date(Date(2015, 12, 6)));
868 	test("2017/11/22 18:00-GMT+00:00", TokenType.dateTime, "2017/11/22 18:00-GMT+00:00", SDLValue.dateTime(SysTime(DateTime(2017, 11, 22, 18, 0, 0), new immutable SimpleTimeZone(0.hours))));
869 	test("2017/11/22 18:00-gmt+00:00", TokenType.invalid, "2017/11/22 18:00-", SDLValue.null_, "", true);
870 
871 	test(" {", TokenType.blockOpen, "{", SDLValue.null_, " ");
872 	test("\t {", TokenType.blockOpen, "{", SDLValue.null_, "\t ");
873 	test("0.5\n", TokenType.number, "0.5", SDLValue(0.5), "", true);
874 }