1 module html.parser; 2 3 4 import std.algorithm; 5 import std.array; 6 import std.ascii; 7 import std.conv; 8 import std.traits; 9 10 import html.entities; 11 import html.utils; 12 13 14 private enum ParserStates { 15 Text = 1, 16 17 // tags 18 PreTagName, 19 TagName, 20 SelfClosingTag, 21 PreClosingTagName, 22 ClosingTagName, 23 PostClosingTagName, 24 25 //attributes 26 PreAttrName, 27 AttrName, 28 PostAttrName, 29 PreAttrValue, 30 AttrValueDQ, 31 AttrValueSQ, 32 AttrValueNQ, 33 34 // decls 35 PreDeclaration, 36 Declaration, 37 ProcessingInstruction, 38 PreComment, 39 Comment, 40 PostComment1, 41 PostComment2, 42 43 // entities 44 PreEntity, 45 PreNumericEntity, 46 NamedEntity, 47 NumericEntity, 48 HexEntity, 49 50 // cdata 51 PreCDATA, 52 PreCDATA_C, 53 PreCDATA_CD, 54 PreCDATA_CDA, 55 PreCDATA_CDAT, 56 PreCDATA_CDATA, 57 CDATA, 58 PostCDATA1, 59 PostCDATA2, 60 61 // scripts / style 62 PreScriptOrStyle, 63 PreScript_SC, 64 PreScript_SCR, 65 PreScript_SCRI, 66 PreScript_SCRIP, 67 PreScript_SCRIPT, 68 PreStyle_ST, 69 PreStyle_STY, 70 PreStyle_STYL, 71 PreStyle_STYLE, 72 PreClosingScriptOrStyle, 73 ClosingScript_SC, 74 ClosingScript_SCR, 75 ClosingScript_SCRI, 76 ClosingScript_SCRIP, 77 ClosingScript_SCRIPT, 78 ClosingStyle_ST, 79 ClosingStyle_STY, 80 ClosingStyle_STYL, 81 ClosingStyle_STYLE, 82 } 83 84 85 private enum ParserTextStates { 86 Normal = 0, 87 Script, 88 Style, 89 } 90 91 92 enum ParserOptions { 93 ParseEntities = 1 << 0, 94 DecodeEntities = 1 << 1, 95 96 None = 0, 97 Default = ParseEntities | DecodeEntities, 98 } 99 100 101 private auto parseNamedEntity(Handler, size_t options)(ref const(char)* start, ref const(char)* ptr, ref Handler handler) { 102 auto length = ptr - start - 1; 103 if (start[1+length-1] == ';') 104 --length; 105 106 if (!length) 107 return false; 108 109 auto limit = min(1+MaxLegacyEntityNameLength, length); 110 auto name = start[1..1+length]; 111 112 while (true) { 113 if (auto pindex = name in index_) { 114 handler.onNamedEntity(name); 115 static if ((options & ParserOptions.DecodeEntities) != 0) { 116 auto offset = codeOffset(*pindex); 117 handler.onEntity(name, cast(const(char)[])bytes_[offset..offset + codeLength(*pindex)]); 118 } 119 120 start += 1 + name.length; 121 return true; 122 } else { 123 if (limit <= MinEntityNameLength) 124 return false; 125 --limit; 126 name = start[1..1+limit]; 127 continue; 128 } 129 } 130 } 131 132 private auto parseNumericEntity(Handler, size_t options)(ref const(char)* start, ref const(char)* ptr, ref Handler handler) { 133 auto length = (ptr - start) - 2; 134 if (start[1+length-1] == ';') 135 --length; 136 137 if (!length) 138 return false; 139 auto name = start[2..2+length]; 140 handler.onNumericEntity(name); 141 142 static if ((options & ParserOptions.DecodeEntities) != 0) { 143 auto cname = name[0..min(8, name.length)]; 144 auto code = parse!uint(cname, 10); 145 handler.onEntity(start[2..2+length], decodeCodePoint(code)); 146 } 147 148 start = ptr; 149 return true; 150 } 151 152 private auto parseHexEntity(Handler, size_t options)(ref const(char)* start, ref const(char)* ptr, ref Handler handler) { 153 auto length = (ptr - start) - 3; 154 if (start[1+length-1] == ';') 155 --length; 156 157 if (!length) 158 return false; 159 160 auto name = start[3..3+length]; 161 handler.onHexEntity(name); 162 163 static if ((options & ParserOptions.DecodeEntities) != 0) { 164 auto cname = name[0..min(6, name.length)]; 165 auto code = parse!uint(cname, 16); 166 handler.onEntity(name, decodeCodePoint(code)); 167 } 168 169 start = ptr; 170 return true; 171 } 172 173 void parseHTML(Handler, size_t options = ParserOptions.Default)(const(char)[] source, ref Handler handler) { 174 auto ptr = source.ptr; 175 auto end = source.ptr + source.length; 176 auto start = ptr; 177 178 ParserStates state = ParserStates.Text; 179 ParserStates saved = ParserStates.Text; 180 ParserTextStates textState = ParserTextStates.Normal; 181 182 enum ParseEntities = (options & (ParserOptions.ParseEntities | ParserOptions.DecodeEntities)) != 0; 183 enum DecodeEntities = (options & ParserOptions.DecodeEntities) != 0; 184 185 while (ptr != end) { 186 final switch(state) with (ParserStates) { 187 case Text: 188 final switch (textState) with (ParserTextStates) { 189 case Normal: 190 static if (ParseEntities) { 191 while ((ptr != end) && (*ptr != '<') && (*ptr != '&')) 192 ++ptr; 193 } else { 194 while ((ptr != end) && (*ptr != '<')) 195 ++ptr; 196 } 197 break; 198 case Script: 199 case Style: 200 while ((ptr != end) && ((*ptr != '<') || (ptr + 1 == end) || (*(ptr + 1) != '/'))) 201 ++ptr; 202 } 203 if (ptr == end) 204 continue; 205 206 static if (ParseEntities) { 207 auto noEntity = *ptr != '&'; 208 } else { 209 enum noEntity = true; 210 } 211 212 if (noEntity) { 213 if (start != ptr) 214 handler.onText(start[0..ptr-start]); 215 state = PreTagName; 216 start = ptr; 217 } else { 218 static if (ParseEntities) { 219 if (start != ptr) 220 handler.onText(start[0..ptr-start]); 221 saved = state; 222 state = PreEntity; 223 start = ptr; 224 } 225 } 226 break; 227 228 case PreTagName: 229 if (*ptr == '/') { 230 state = PreClosingTagName; 231 } else if ((*ptr == '>') || isWhite(*ptr) || (textState != ParserTextStates.Normal)) { 232 state = Text; 233 } else { 234 switch (*ptr) { 235 case '!': 236 state = PreDeclaration; 237 start = ptr + 1; 238 break; 239 case '?': 240 state = ProcessingInstruction; 241 start = ptr + 1; 242 break; 243 case '<': 244 handler.onText(start[0..ptr-start]); 245 start = ptr + 1; 246 break; 247 default: 248 if ((*ptr == 's') || (*ptr == 'S')) { 249 state = PreScriptOrStyle; 250 } else { 251 state = TagName; 252 } 253 start = ptr; 254 break; 255 } 256 } 257 break; 258 259 case PreScriptOrStyle: 260 if ((*ptr == 'c') || (*ptr == 'C')) { 261 state = PreScript_SC; 262 } else if ((*ptr == 't') || (*ptr == 'T')) { 263 state = PreStyle_ST; 264 } else { 265 state = TagName; 266 goto case TagName; 267 } 268 break; 269 270 case TagName: 271 while ((ptr != end) && (*ptr != '/') && (*ptr != '>') && !isWhite(*ptr)) 272 ++ptr; 273 if (ptr == end) 274 continue; 275 276 handler.onOpenStart(start[0..ptr-start]); 277 state = PreAttrName; 278 continue; 279 280 case PreClosingTagName: 281 while ((ptr != end) && isWhite(*ptr)) 282 ++ptr; 283 if (ptr == end) 284 continue; 285 286 if (*ptr == '>') { 287 state = Text; 288 } else if (textState != ParserTextStates.Normal) { 289 if ((*ptr == 's') || (*ptr == 'S')) { 290 state = PreClosingScriptOrStyle; 291 } else { 292 state = Text; 293 continue; 294 } 295 } else { 296 state = ClosingTagName; 297 start = ptr; 298 } 299 break; 300 301 case PreClosingScriptOrStyle: 302 if ((textState == ParserTextStates.Script) && ((*ptr == 'c') || (*ptr == 'C'))) { 303 state = ClosingScript_SC; 304 } else if ((textState == ParserTextStates.Style) && ((*ptr == 't') || (*ptr == 'T'))) { 305 state = ClosingStyle_ST; 306 } else { 307 state = Text; 308 goto case Text; 309 } 310 break; 311 312 case ClosingTagName: 313 while ((ptr != end) && (*ptr != '>') && !isWhite(*ptr)) 314 ++ptr; 315 if (ptr == end) 316 continue; 317 318 handler.onClose(start[0..ptr-start]); 319 state = PostClosingTagName; 320 continue; 321 322 case PostClosingTagName: 323 while ((ptr != end) && (*ptr != '>')) 324 ++ptr; 325 if (ptr == end) 326 continue; 327 328 state = Text; 329 start = ptr + 1; 330 break; 331 332 case SelfClosingTag: 333 while ((ptr != end) && (*ptr != '>') && isWhite(*ptr)) 334 ++ptr; 335 if (ptr == end) 336 continue; 337 338 if (*ptr == '>') { 339 handler.onSelfClosing(); 340 state = Text; 341 start = ptr + 1; 342 } else { 343 state = PreAttrName; 344 continue; 345 } 346 break; 347 348 case PreAttrName: 349 while ((ptr != end) && (*ptr != '>') && (*ptr != '/') && isWhite(*ptr)) 350 ++ptr; 351 if (ptr == end) 352 continue; 353 354 if (*ptr == '>') { 355 handler.onOpenEnd(start[0..ptr-start]); 356 state = Text; 357 start = ptr + 1; 358 } else if (*ptr == '/') { 359 state = SelfClosingTag; 360 } else { 361 state = AttrName; 362 start = ptr; 363 } 364 break; 365 366 case AttrName: 367 while ((ptr != end) && (*ptr != '=') && (*ptr != '>') && (*ptr != '/') && !isWhite(*ptr)) 368 ++ptr; 369 if (ptr == end) 370 continue; 371 372 handler.onAttrName(start[0..ptr-start]); 373 state = PostAttrName; 374 start = ptr; 375 continue; 376 377 case PostAttrName: 378 while ((ptr != end) && (*ptr != '=') && (*ptr != '>') && (*ptr != '/') && isWhite(*ptr)) 379 ++ptr; 380 if (ptr == end) 381 continue; 382 383 switch(*ptr) { 384 case '=': 385 state = PreAttrValue; 386 break; 387 case '/': 388 case '>': 389 handler.onAttrEnd(); 390 state = PreAttrName; 391 continue; 392 default: 393 handler.onAttrEnd(); 394 state = PreAttrName; 395 start = ptr; 396 continue; 397 } 398 break; 399 400 case PreAttrValue: 401 while ((ptr != end) && (*ptr != '\"') && (*ptr != '\'') && isWhite(*ptr)) 402 ++ptr; 403 if (ptr == end) 404 continue; 405 406 switch(*ptr) { 407 case '\"': 408 state = AttrValueDQ; 409 start = ptr + 1; 410 break; 411 case '\'': 412 state = AttrValueSQ; 413 start = ptr + 1; 414 break; 415 default: 416 state = AttrValueNQ; 417 start = ptr; 418 continue; 419 } 420 break; 421 422 case AttrValueDQ: 423 static if (ParseEntities) { 424 while ((ptr != end) && (*ptr != '\"') && (*ptr != '&')) 425 ++ptr; 426 } else { 427 while ((ptr != end) && (*ptr != '\"')) 428 ++ptr; 429 } 430 if (ptr == end) 431 continue; 432 433 static if (ParseEntities) { 434 auto noEntity = *ptr != '&'; 435 } else { 436 enum noEntity = true; 437 } 438 439 if (noEntity) { 440 handler.onAttrValue(start[0..ptr-start]); 441 handler.onAttrEnd(); 442 state = PreAttrName; 443 } else { 444 static if (ParseEntities) { 445 if (start != ptr) 446 handler.onAttrValue(start[0..ptr-start]); 447 saved = state; 448 state = PreEntity; 449 start = ptr; 450 } 451 } 452 break; 453 454 case AttrValueSQ: 455 static if (ParseEntities) { 456 while ((ptr != end) && (*ptr != '\'') && (*ptr != '&')) 457 ++ptr; 458 } else { 459 while ((ptr != end) && (*ptr != '\'')) 460 ++ptr; 461 } 462 if (ptr == end) 463 continue; 464 465 static if (ParseEntities) { 466 auto noEntity = *ptr != '&'; 467 } else { 468 enum noEntity = true; 469 } 470 471 if (noEntity) { 472 handler.onAttrValue(start[0..ptr-start]); 473 handler.onAttrEnd(); 474 state = PreAttrName; 475 } else { 476 static if (ParseEntities) { 477 if (start != ptr) 478 handler.onAttrValue(start[0..ptr-start]); 479 saved = state; 480 state = PreEntity; 481 start = ptr; 482 } 483 } 484 break; 485 486 case AttrValueNQ: 487 static if (ParseEntities) { 488 while ((ptr != end) && (*ptr != '>') && (*ptr != '&') && !isWhite(*ptr)) 489 ++ptr; 490 } else { 491 while ((ptr != end) && (*ptr != '>') && !isWhite(*ptr)) 492 ++ptr; 493 } 494 if (ptr == end) 495 continue; 496 497 static if (ParseEntities) { 498 auto noEntity = *ptr != '&'; 499 } else { 500 enum noEntity = true; 501 } 502 503 if (noEntity) { 504 handler.onAttrValue(start[0..ptr-start]); 505 handler.onAttrEnd(); 506 state = PreAttrName; 507 } else { 508 static if (ParseEntities) { 509 if (start != ptr) 510 handler.onAttrValue(start[0..ptr-start]); 511 saved = state; 512 state = PreEntity; 513 start = ptr; 514 break; 515 } 516 } 517 continue; 518 519 case PreComment: 520 if (*ptr == '-') { 521 state = Comment; 522 start = ptr + 1; 523 } else { 524 state = Declaration; 525 } 526 break; 527 528 case Comment: 529 while ((ptr != end) && (*ptr != '-')) 530 ++ptr; 531 if (ptr == end) 532 continue; 533 534 state = PostComment1; 535 break; 536 537 case PostComment1: 538 state = (*ptr == '-') ? PostComment2 : Comment; 539 break; 540 541 case PostComment2: 542 if (*ptr == '>') { 543 handler.onComment(start[0..ptr-start-2]); 544 state = Text; 545 start = ptr + 1; 546 } else if (*ptr != '-') { 547 state = Comment; 548 } 549 break; 550 551 case PreDeclaration: 552 switch(*ptr) { 553 case '[': 554 state = PreCDATA; 555 break; 556 case '-': 557 state = PreComment; 558 break; 559 default: 560 state = Declaration; 561 break; 562 } 563 break; 564 565 case PreCDATA: 566 if ((*ptr == 'C') || (*ptr == 'c')) { 567 state = PreCDATA_C; 568 } else { 569 state = Declaration; 570 continue; 571 } 572 break; 573 574 case PreCDATA_C: 575 if ((*ptr == 'D') || (*ptr == 'd')) { 576 state = PreCDATA_CD; 577 } else { 578 state = Declaration; 579 continue; 580 } 581 break; 582 583 case PreCDATA_CD: 584 if ((*ptr == 'A') || (*ptr == 'a')) { 585 state = PreCDATA_CDA; 586 } else { 587 state = Declaration; 588 continue; 589 } 590 break; 591 592 case PreCDATA_CDA: 593 if ((*ptr == 'T') || (*ptr == 't')) { 594 state = PreCDATA_CDAT; 595 } else { 596 state = Declaration; 597 continue; 598 } 599 break; 600 601 case PreCDATA_CDAT: 602 if ((*ptr == 'A') || (*ptr == 'a')) { 603 state = PreCDATA_CDATA; 604 } else { 605 state = Declaration; 606 continue; 607 } 608 break; 609 610 case PreCDATA_CDATA: 611 if (*ptr == '[') { 612 state = CDATA; 613 start = ptr + 1; 614 } else { 615 state = Declaration; 616 continue; 617 } 618 break; 619 620 case CDATA: 621 while ((ptr != end) && (*ptr != ']')) 622 ++ptr; 623 if (ptr == end) 624 continue; 625 626 state = PostCDATA1; 627 break; 628 629 case PostCDATA1: 630 state = (*ptr == ']') ? PostCDATA2 : CDATA; 631 break; 632 633 case PostCDATA2: 634 if (*ptr == '>') { 635 handler.onCDATA(start[0..ptr-start-2]); 636 state = Text; 637 start = ptr + 1; 638 } else if (*ptr != ']') { 639 state = CDATA; 640 } 641 break; 642 643 case Declaration: 644 while ((ptr != end) && (*ptr != '>')) 645 ++ptr; 646 if (ptr == end) 647 continue; 648 649 handler.onDeclaration(start[0..ptr-start]); 650 state = Text; 651 start = ptr + 1; 652 break; 653 654 case ProcessingInstruction: 655 while ((ptr != end) && (*ptr != '>')) 656 ++ptr; 657 if (ptr == end) 658 continue; 659 660 handler.onProcessingInstruction(start[0..ptr-start]); 661 state = Text; 662 start = ptr + 1; 663 break; 664 665 case PreEntity: 666 static if (ParseEntities) { 667 if (*ptr == '#') { 668 state = PreNumericEntity; 669 break; 670 } else { 671 state = NamedEntity; 672 continue; 673 } 674 } else { 675 assert(0, "should never get here!"); 676 } 677 678 case NamedEntity: 679 static if (ParseEntities) { 680 while ((ptr != end) && (*ptr != ';') && isAlphaNum(*ptr) && (ptr - start < MaxEntityNameLength)) 681 ++ptr; 682 if (ptr == end) 683 continue; 684 685 if ((saved == Text) || (*ptr != '=')) { 686 if (parseNamedEntity!(Handler, options)(start, ptr, handler)) { 687 if (*start == ';') 688 ++start; 689 } 690 } 691 state = saved; 692 693 if (*ptr == ';') break; 694 else continue; 695 } else { 696 break; 697 } 698 699 case PreNumericEntity: 700 static if (ParseEntities) { 701 if ((*ptr == 'X') || (*ptr == 'x')) { 702 state = HexEntity; 703 break; 704 } else { 705 state = NumericEntity; 706 continue; 707 } 708 } else { 709 assert(0, "should never get here!"); 710 } 711 712 case NumericEntity: 713 static if (ParseEntities) { 714 while ((ptr != end) && (*ptr != ';') && isDigit(*ptr)) 715 ++ptr; 716 if (ptr == end) 717 continue; 718 719 state = saved; 720 if (parseNumericEntity!(Handler, options)(start, ptr, handler)) { 721 if (*start == ';') 722 ++start; 723 } 724 725 if (*ptr == ';') break; 726 else continue; 727 } else { 728 break; 729 } 730 731 case HexEntity: 732 static if (ParseEntities) { 733 while ((ptr != end) && (*ptr != ';') && isHexDigit(*ptr)) 734 ++ptr; 735 if (ptr == end) 736 continue; 737 738 state = saved; 739 if (parseHexEntity!(Handler, options)(start, ptr, handler)) { 740 if (*start == ';') 741 ++start; 742 } 743 744 if (*ptr == ';') break; 745 else continue; 746 } else { 747 break; 748 } 749 750 case PreScript_SC: 751 if ((*ptr == 'r') || (*ptr == 'R')) { 752 state = PreScript_SCR; 753 } else { 754 state = TagName; 755 goto case TagName; 756 } 757 break; 758 759 case PreScript_SCR: 760 if ((*ptr == 'i') || (*ptr == 'I')) { 761 state = PreScript_SCRI; 762 } else { 763 state = TagName; 764 goto case TagName; 765 } 766 break; 767 768 case PreScript_SCRI: 769 if ((*ptr == 'p') || (*ptr == 'p')) { 770 state = PreScript_SCRIP; 771 } else { 772 state = TagName; 773 goto case TagName; 774 } 775 break; 776 777 case PreScript_SCRIP: 778 if ((*ptr == 't') || (*ptr == 't')) { 779 state = PreScript_SCRIPT; 780 } else { 781 state = TagName; 782 goto case TagName; 783 } 784 break; 785 786 case PreScript_SCRIPT: 787 if ((*ptr == '/') || (*ptr == '>') || isWhite(*ptr)) 788 textState = ParserTextStates.Script; 789 790 state = TagName; 791 goto case TagName; 792 793 case PreStyle_ST: 794 if ((*ptr == 'y') || (*ptr == 'Y')) { 795 state = PreStyle_STY; 796 } else { 797 state = TagName; 798 goto case TagName; 799 } 800 break; 801 802 case PreStyle_STY: 803 if ((*ptr == 'l') || (*ptr == 'L')) { 804 state = PreStyle_STYL; 805 } else { 806 state = TagName; 807 goto case TagName; 808 } 809 break; 810 811 case PreStyle_STYL: 812 if ((*ptr == 'e') || (*ptr == 'E')) { 813 state = PreStyle_STYLE; 814 } else { 815 state = TagName; 816 goto case TagName; 817 } 818 break; 819 820 case PreStyle_STYLE: 821 if ((*ptr == '/') || (*ptr == '>') || isWhite(*ptr)) 822 textState = ParserTextStates.Style; 823 824 state = TagName; 825 goto case TagName; 826 827 case ClosingScript_SC: 828 if ((*ptr == 'r') || (*ptr == 'R')) { 829 state = ClosingScript_SCR; 830 } else { 831 state = Text; 832 goto case Text; 833 } 834 break; 835 836 case ClosingScript_SCR: 837 if ((*ptr == 'i') || (*ptr == 'I')) { 838 state = ClosingScript_SCRI; 839 } else { 840 state = Text; 841 goto case Text; 842 } 843 break; 844 845 case ClosingScript_SCRI: 846 if ((*ptr == 'p') || (*ptr == 'p')) { 847 state = ClosingScript_SCRIP; 848 } else { 849 state = Text; 850 goto case Text; 851 } 852 break; 853 854 case ClosingScript_SCRIP: 855 if ((*ptr == 't') || (*ptr == 't')) { 856 state = ClosingScript_SCRIPT; 857 } else { 858 state = Text; 859 goto case Text; 860 } 861 break; 862 863 case ClosingScript_SCRIPT: 864 if ((*ptr == '>') || isWhite(*ptr)) { 865 textState = ParserTextStates.Normal; 866 state = ClosingTagName; 867 start += 2; 868 continue; 869 } else { 870 state = Text; 871 goto case Text; 872 } 873 874 case ClosingStyle_ST: 875 if ((*ptr == 'y') || (*ptr == 'Y')) { 876 state = ClosingStyle_STY; 877 } else { 878 state = Text; 879 goto case Text; 880 } 881 break; 882 883 case ClosingStyle_STY: 884 if ((*ptr == 'l') || (*ptr == 'L')) { 885 state = ClosingStyle_STYL; 886 } else { 887 state = Text; 888 goto case Text; 889 } 890 break; 891 892 case ClosingStyle_STYL: 893 if ((*ptr == 'e') || (*ptr == 'E')) { 894 state = ClosingStyle_STYLE; 895 } else { 896 state = Text; 897 goto case Text; 898 } 899 break; 900 901 case ClosingStyle_STYLE: 902 if ((*ptr == '>') || isWhite(*ptr)) { 903 textState = ParserTextStates.Normal; 904 state = ClosingTagName; 905 start += 2; 906 continue; 907 } else { 908 state = Text; 909 goto case Text; 910 } 911 } 912 913 ++ptr; 914 } 915 916 auto remaining = start[0..ptr-start]; 917 if (!remaining.empty) { 918 switch(state) with (ParserStates) { 919 case Comment: 920 handler.onComment(remaining); 921 break; 922 case PostComment1: 923 handler.onComment(remaining[0..$-1]); 924 break; 925 case PostComment2: 926 handler.onComment(remaining[0..$-2]); 927 break; 928 case NamedEntity: 929 static if (ParseEntities) { 930 if (saved == Text) { 931 if (ptr-start > 1) 932 parseNamedEntity!(Handler, options)(start, ptr, handler); 933 if (start < ptr) 934 handler.onText(start[0..ptr-start]); 935 } 936 } 937 break; 938 case NumericEntity: 939 static if (ParseEntities) { 940 if (saved == Text) { 941 if (ptr-start > 2) 942 parseNumericEntity!(Handler, options)(start, ptr, handler); 943 if (start < ptr) 944 handler.onText(start[0..ptr-start]); 945 } 946 } 947 break; 948 case HexEntity: 949 static if (ParseEntities) { 950 if (saved == Text) { 951 if (ptr-start > 3) 952 parseHexEntity!(Handler, options)(start, ptr, handler); 953 if (start < ptr) 954 handler.onText(start[0..ptr-start]); 955 } 956 } 957 break; 958 default: 959 if ((state != TagName) && 960 (state != PreAttrName) && 961 (state != PreAttrValue) && 962 (state != PostAttrName) && 963 (state != AttrName) && 964 (state != AttrValueDQ) && 965 (state != AttrValueSQ) && 966 (state != AttrValueNQ) && 967 (state != ClosingTagName)) { 968 handler.onText(remaining); 969 } 970 break; 971 } 972 } 973 974 handler.onDocumentEnd(); 975 }