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 }