1 /** 2 * Copyright © DiamondMVC 2019 3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module diamond.dom.domnode; 7 8 import std..string : strip, toLower; 9 import std.algorithm : filter; 10 import std.array : array; 11 12 import diamond.errors.checks; 13 import diamond.dom.domattribute; 14 import diamond.dom.domexception; 15 import diamond.dom.domparsersettings; 16 17 private size_t _nextId; 18 19 /// A dom node. 20 final class DomNode 21 { 22 private: 23 /// The id of the node. 24 size_t _nodeId; 25 /// The name of the node. 26 string _name; 27 /// The parent of the node. 28 DomNode _parent; 29 /// The text of the node. 30 string _text; 31 /// The attributes of the node. 32 DomAttribute[string] _attributes; 33 /// The children of the node. 34 DomNode[] _children; 35 /// The parser settings used for the dom node. 36 DomParserSettings _parserSettings; 37 /// Boolean determining whether the node is a text node or not. 38 bool _textNode; 39 40 public: 41 final: 42 /** 43 * Creates a new dom node. 44 * Params: 45 * parent = The parent node. 46 */ 47 this(DomNode parent) @safe 48 { 49 _nodeId = _nextId; 50 _nextId++; 51 52 _parent = parent; 53 } 54 55 @property 56 { 57 /// Gets the name of the node. 58 string name() @safe { return _name; } 59 60 /// Sets the name of the node. 61 void name(string newName) @safe 62 { 63 enforce(newName !is null, "The name cannot be null."); 64 65 _name = newName.strip(); 66 } 67 68 /// Gets the text of the node. 69 string text() { return _text; } 70 71 /// Sets the text of the node. If the text contains valid dom, then it'll be parsed. To just set the text without parsing use rawText. 72 void text(string newText) @safe 73 { 74 import diamond.dom.domparser; 75 76 auto elements = parseDomElements(newText, _parserSettings); 77 78 if (elements && elements.length) 79 { 80 foreach (element; elements) 81 { 82 element._parent = this; 83 } 84 85 _children = elements; 86 } 87 else if (newText) 88 { 89 _text = newText.strip(); 90 _children = null; 91 } 92 } 93 94 /// Sets the raw text of the node. 95 package(diamond.dom) void rawText(string text) @safe 96 { 97 enforce(text !is null, "There must be a text specified."); 98 99 _text = text.strip(); 100 } 101 102 /// Gets the parent of the node. 103 DomNode parent() @safe { return _parent; } 104 105 /// Gets the children of the node. 106 DomNode[] children() @safe { return _children; } 107 108 package(diamond) 109 { 110 /// Gets the settings used to parse the dom node. 111 DomParserSettings parserSettings() @safe { return _parserSettings; } 112 113 /// Sets the settings used to parse the dom node. 114 void parserSettings(DomParserSettings parserSettings) @safe 115 { 116 _parserSettings = parserSettings; 117 } 118 } 119 120 package(diamond.dom) 121 { 122 /// Gets a boolean determining whether the node is a text node or not. 123 bool isTextNode() @safe { return _textNode; } 124 125 /// Sets a boolean determining whether the node is a text node or not. 126 void isTextNode(bool textNode) @safe 127 { 128 _textNode = textNode; 129 } 130 } 131 } 132 133 /** 134 * Adds an attribute to the node. 135 * Params: 136 * attribute = The attribute to add. 137 */ 138 void addAttribute(DomAttribute attribute) @safe 139 { 140 if (!attribute) 141 { 142 throw new DomException("Cannot add a null attribute."); 143 } 144 145 _attributes[attribute.name] = attribute; 146 } 147 148 /** 149 * Adds an attribute to the node. 150 * Params: 151 * name = The name of the attribute. 152 * value = The value of the attribute. 153 */ 154 void addAttribute(string name, string value) @safe 155 { 156 enforce(name !is null, "The name cannot be null."); 157 158 name = name.strip().toLower(); 159 160 if (!name.length) 161 { 162 return; 163 } 164 165 _attributes[name] = new DomAttribute(name, value); 166 } 167 168 /** 169 * Removes an attribute from the node. 170 * Params: 171 * name = The name of the attribute to remove. 172 */ 173 void removeAttribute(string name) @safe 174 { 175 if (!name) 176 { 177 return; 178 } 179 180 name = name.strip().toLower(); 181 182 _attributes.remove(name); 183 } 184 185 /** 186 * Gets an attribute from the node. 187 * Params: 188 * name = The name of the attribute to retrieve. 189 * Returns: 190 * The attribute if present, otherwise null. 191 */ 192 DomAttribute getAttribute(string name) @safe 193 { 194 if (!name) 195 { 196 return null; 197 } 198 199 name = name.strip().toLower(); 200 201 return _attributes.get(name, null); 202 } 203 204 /** 205 * Checks whether an attribute is present within the node or not. 206 * Params: 207 * name = The name of the attribute. 208 * Returns: 209 * True if the attribute is present, false otherwise. 210 */ 211 bool hasAttribute(string name) @trusted 212 { 213 if (!name) 214 { 215 return false; 216 } 217 218 name = name.strip().toLower(); 219 220 return cast(bool)(name in _attributes); 221 } 222 223 /** 224 * Checks whether an attribute is present within the node or not. 225 * Params: 226 * name = The name of the attribute. 227 * value = The value of the attribute. 228 * Returns: 229 * True if the attribute is present, false otherwise. 230 */ 231 bool hasAttribute(string name, string value) @trusted 232 { 233 if (!name) 234 { 235 return false; 236 } 237 238 name = name.strip().toLower(); 239 240 auto attribute = _attributes.get(name, null); 241 242 if (!attribute) 243 { 244 return false; 245 } 246 247 return attribute.value == value; 248 } 249 250 /** 251 * Checks whether an attribute is present within the node or not and contains the value as a word. 252 * Params: 253 * name = The name of the attribute. 254 * value = The value of the attribute. 255 * Returns: 256 * True if the attribute is present, false otherwise. 257 */ 258 bool hasAttributeContains(string name, string value) @trusted 259 { 260 if (!name) 261 { 262 return false; 263 } 264 265 name = name.strip().toLower(); 266 267 auto attribute = _attributes.get(name, null); 268 269 if (!attribute) 270 { 271 return false; 272 } 273 274 import std.array : split, array; 275 import std.algorithm : filter, map, canFind; 276 277 return attribute.value.split(" ").filter!(v => v && v.strip().length).map!(v => v.strip()).canFind(value); 278 } 279 280 /** 281 * Checks whether an attribute is present within the node or not and starts with the value given. 282 * Params: 283 * name = The name of the attribute. 284 * value = The value of the attribute. 285 * Returns: 286 * True if the attribute is present, false otherwise. 287 */ 288 bool hasAttributeStartsWith(string name, string value) @trusted 289 { 290 if (!name) 291 { 292 return false; 293 } 294 295 name = name.strip().toLower(); 296 297 auto attribute = _attributes.get(name, null); 298 299 if (!attribute) 300 { 301 return false; 302 } 303 304 import std.algorithm : startsWith; 305 306 return attribute.value.startsWith(value); 307 } 308 309 /** 310 * Checks whether an attribute is present within the node or not and ends with the value given. 311 * Params: 312 * name = The name of the attribute. 313 * value = The value of the attribute. 314 * Returns: 315 * True if the attribute is present, false otherwise. 316 */ 317 bool hasAttributeEndsWith(string name, string value) @trusted 318 { 319 if (!name) 320 { 321 return false; 322 } 323 324 name = name.strip().toLower(); 325 326 auto attribute = _attributes.get(name, null); 327 328 if (!attribute) 329 { 330 return false; 331 } 332 333 import std.algorithm : endsWith; 334 335 return attribute.value.endsWith(value); 336 } 337 338 /** 339 * Checks whether an attribute is present within the node or not and contains a substring with the value given. 340 * Params: 341 * name = The name of the attribute. 342 * value = The value of the attribute. 343 * Returns: 344 * True if the attribute is present, false otherwise. 345 */ 346 bool hasAttributeSubstring(string name, string value) @trusted 347 { 348 if (!name) 349 { 350 return false; 351 } 352 353 name = name.strip().toLower(); 354 355 auto attribute = _attributes.get(name, null); 356 357 if (!attribute) 358 { 359 return false; 360 } 361 362 import std.algorithm : canFind; 363 364 return attribute.value.canFind(value); 365 } 366 367 /// Clears the attributes of the node. 368 void clearAttributes() 369 { 370 if (_attributes) 371 { 372 _attributes.clear(); 373 } 374 } 375 376 /** 377 * Gets all attributes. 378 * Returns: 379 * An array of the attribute. 380 */ 381 DomAttribute[] getAttributes() @trusted 382 { 383 return _attributes ? _attributes.values : []; 384 } 385 386 /** 387 * Gets all attribute names. 388 * Returns: 389 * An array of the attribute names. 390 */ 391 string[] getAttributeNames() @trusted 392 { 393 return _attributes ? _attributes.keys : []; 394 } 395 396 /** 397 * Adds a child to the dom node. 398 * Params: 399 * child = The child to add. 400 */ 401 void addChild(DomNode child) @safe 402 { 403 if (!child) 404 { 405 throw new DomException("Cannot add a null child."); 406 } 407 408 _children ~= child; 409 } 410 411 /** 412 * Gets all nodes by a tag name. 413 * Params: 414 * tagName = The tag name to retrieve nodes by. 415 * searchChildren = If set to true, then it'll perform a nested search through children. 416 * Returns: 417 * An array of the nodes found. 418 */ 419 DomNode[] getByTagName(string tagName, bool searchChildren = false) @safe 420 { 421 enforce(tagName !is null, "The tag cannot be null."); 422 423 if (!searchChildren) 424 { 425 return _children ? (() @trusted { return _children.filter!(c => c.name.toLower() == tagName.toLower()).array; })() : []; 426 } 427 428 DomNode[] elements; 429 430 tagName = tagName.strip().toLower(); 431 432 foreach (child; _children) 433 { 434 if (child.name.toLower() == tagName) 435 { 436 elements ~= child; 437 } 438 439 if (searchChildren) 440 { 441 elements ~= child.getByTagName(tagName, searchChildren); 442 } 443 } 444 445 return elements ? elements : []; 446 } 447 448 /** 449 * Gets all nodes by an attribute. 450 * Params: 451 * name = The attribute name to retrieve nodes by. 452 * value = The value to retrieve nodes by. 453 * searchChildren = If set to true, then it'll perform a nested search through children. 454 * Returns: 455 * An array of the nodes found. 456 */ 457 DomNode[] getByAttribute(string name, string value, bool searchChildren = false) @safe 458 { 459 enforce(name !is null, "The name cannot be null."); 460 enforce(value !is null, "The value cannot be null."); 461 462 DomNode[] elements; 463 464 name = name.strip().toLower(); 465 value = value.strip(); 466 467 foreach (child; _children) 468 { 469 auto attribute = child.getAttribute(name); 470 471 if (attribute && value == (attribute.value ? attribute.value.strip() : attribute.value)) 472 { 473 elements ~= child; 474 } 475 476 if (searchChildren) 477 { 478 elements ~= child.getByAttribute(name, value, searchChildren); 479 } 480 } 481 482 return elements; 483 } 484 485 /** 486 * Gets all nodes by an attribute name. 487 * Params: 488 * name = The attribute name to retrieve nodes by. 489 * searchChildren = If set to true, then it'll perform a nested search through children. 490 * Returns: 491 * An array of the nodes found. 492 */ 493 DomNode[] getByAttributeName(string name, bool searchChildren = false) @safe 494 { 495 enforce(name !is null, "The name cannot be null."); 496 497 DomNode[] elements; 498 499 name = name.strip().toLower(); 500 501 foreach (child; _children) 502 { 503 if (child.hasAttribute(name)) 504 { 505 elements ~= child; 506 } 507 508 if (searchChildren) 509 { 510 elements ~= child.getByAttributeName(name, searchChildren); 511 } 512 } 513 514 return elements; 515 } 516 517 /** 518 * Gets a dom node by an attribute named "id" matching the given value. 519 * Params: 520 * id = The id of the node to retrieve. 521 * Returns: 522 * The dom node if found, null otherwise. 523 */ 524 DomNode getElementById(string id) @safe 525 { 526 if (_children && _children.length) 527 { 528 foreach (child; _children) 529 { 530 auto result = child.getByAttribute("id", id, true); 531 532 if (result && result.length) 533 { 534 return result[0]; 535 } 536 } 537 } 538 539 return null; 540 } 541 542 /** 543 * Queries all dom nodes based on a css3 selector. 544 * Params: 545 * selector = The css3 selector. 546 * Returns: 547 * An array of all matching nodes. 548 */ 549 DomNode[] querySelectorAll(string selector) 550 { 551 import std.array : split, array; 552 import std.algorithm : map, filter, sort, group; 553 554 import diamond.css; 555 556 auto selectorCollection = selector.split(","); 557 558 DomNode[] elements; 559 bool hadWildCard; 560 561 foreach (cssSelector; selectorCollection) 562 { 563 if (hadWildCard) 564 { 565 break; 566 } 567 568 DomNode[] selectorElements; 569 570 auto query = parseCss3Selector(cssSelector); 571 572 if (query) 573 { 574 Css3SelectionQuery current = query; 575 Css3SelectionQuery last; 576 577 DomNode[] currentNodes = [this]; 578 579 while (current && !hadWildCard) 580 { 581 Css3SelectorOperator operator = current.operator; 582 583 if (operator == Css3SelectorOperator.none) 584 { 585 if (!last) 586 { 587 break; 588 } 589 590 operator = last.operator; 591 } 592 593 if (current.selections) 594 { 595 auto nodes = currentNodes.dup; 596 currentNodes = []; 597 598 foreach (selection; current.selections) 599 { 600 if (selection.hasWildcard) 601 { 602 foreach (currentNode; nodes) 603 { 604 elements = [currentNode]; 605 elements ~= currentNode.getAll(); 606 } 607 608 hadWildCard = true; 609 break; 610 } 611 612 foreach (currentNode; nodes) 613 { 614 DomNode[] filterNodes(DomNode[] temp) @safe 615 { 616 foreach (tagName; selection.tagNames) 617 { 618 temp = temp.filter!(n => n.name == tagName).array; 619 } 620 621 foreach (id; selection.ids) 622 { 623 temp = temp.filter!(n => n.hasAttribute("id", id)).array; 624 } 625 626 foreach (className; selection.classNames) 627 { 628 temp = temp.filter!(n => n.hasAttributeContains("class", className)).array; 629 } 630 631 if (selection.attributeSelection) 632 { 633 auto attribute = selection.attributeSelection; 634 635 switch (attribute.operator) 636 { 637 case Css3SelectorAttributeOperator.equals: 638 { 639 temp = temp.filter!(n => n.hasAttribute(attribute.name, attribute.value)).array; 640 break; 641 } 642 643 case Css3SelectorAttributeOperator.containsWord: 644 { 645 temp = temp.filter!(n => n.hasAttributeContains(attribute.name, attribute.value)).array; 646 break; 647 } 648 649 case Css3SelectorAttributeOperator.listStartsWith: 650 { 651 auto values = attribute.value.split("-"); 652 653 foreach (value; values) 654 { 655 temp = temp.filter!(n => n.hasAttributeContains(attribute.name, value)).array; 656 } 657 break; 658 } 659 660 case Css3SelectorAttributeOperator.startsWith: 661 { 662 temp = temp.filter!(n => n.hasAttributeStartsWith(attribute.name, attribute.value)).array; 663 break; 664 } 665 666 case Css3SelectorAttributeOperator.endsWith: 667 { 668 temp = temp.filter!(n => n.hasAttributeEndsWith(attribute.name, attribute.value)).array; 669 break; 670 } 671 672 case Css3SelectorAttributeOperator.contains: 673 { 674 temp = temp.filter!(n => n.hasAttributeSubstring(attribute.name, attribute.value)).array; 675 break; 676 } 677 678 default: break; 679 } 680 } 681 682 return temp; 683 } 684 685 switch (operator) 686 { 687 case Css3SelectorOperator.firstChild: 688 { 689 if (currentNode._children) 690 { 691 DomNode[] temp = currentNode._children.dup; 692 693 temp = filterNodes(temp); 694 695 if (temp && temp.length) 696 { 697 currentNodes ~= temp[0]; 698 } 699 } 700 break; 701 } 702 703 case Css3SelectorOperator.firstSibling: 704 { 705 if (currentNode._parent && currentNode._parent._children) 706 { 707 DomNode[] temp = currentNode._parent._children.dup; 708 709 temp = filterNodes(temp); 710 711 if (temp && temp.length) 712 { 713 currentNodes ~= temp[0]; 714 } 715 } 716 break; 717 } 718 719 case Css3SelectorOperator.allChildren: 720 { 721 if (currentNode._children) 722 { 723 DomNode[] temp = currentNode.getAll().dup; 724 725 temp = filterNodes(temp); 726 727 if (temp && temp.length) 728 { 729 currentNodes ~= temp; 730 } 731 } 732 break; 733 } 734 735 case Css3SelectorOperator.allSiblings: 736 { 737 if (currentNode._parent && currentNode._parent._children) 738 { 739 DomNode[] temp = currentNode._parent._children.dup; 740 741 temp = filterNodes(temp); 742 743 if (temp && temp.length) 744 { 745 currentNodes ~= temp; 746 } 747 } 748 break; 749 } 750 751 default: break; 752 } 753 } 754 } 755 756 if (hadWildCard) 757 { 758 current = null; 759 continue; 760 } 761 } 762 763 last = current; 764 current = current.nextSelection; 765 } 766 767 selectorElements = currentNodes; 768 } 769 770 elements ~= selectorElements; 771 } 772 773 return elements ? elements.sort.group.map!(g => g[0]).array : []; 774 } 775 776 /** 777 * Queries the first dom node based on a css3 selector. 778 * Params: 779 * selector = The css3 selector. 780 * Returns: 781 * The node if found, null otherwise. 782 */ 783 DomNode querySelector(string selector) 784 { 785 auto result = querySelectorAll(selector); 786 787 if (!result || !result.length) 788 { 789 return null; 790 } 791 792 return result[0]; 793 } 794 795 /** 796 * Gets all nested nodes. 797 * Returns: 798 * An array of all nested nodes. 799 */ 800 private DomNode[] getAll() 801 { 802 DomNode[] nodes; 803 804 if (_children) 805 { 806 foreach (child; _children) 807 { 808 nodes ~= child; 809 810 nodes ~= child.getAll(); 811 } 812 } 813 814 return nodes ? nodes : []; 815 } 816 817 /** 818 * Converts the node to a properly formatted dom outer node-string. 819 * Returns: 820 * A string equivalent to the properly formatted dom outer node-string. 821 */ 822 string toOuterString() @trusted 823 { 824 import std..string : format; 825 import std.algorithm : map, filter; 826 import std.array : array, join; 827 828 string attributes; 829 if (_attributes && _attributes.length) 830 { 831 attributes = " "; 832 833 attributes ~= _attributes.values.filter!(a => a.name && a.name.strip().length).map!(a => "%s=\"%s\"".format(a.name, a.value)).array.join(" "); 834 } 835 836 if (_parserSettings && _parserSettings.isSelfClosingTag(_name)) 837 { 838 return "<%s%s>\r\n".format(_name, attributes); 839 } 840 else if (_parserSettings && _parserSettings.isStandardTag(_name)) 841 { 842 return "<%s%s></%s>\r\n".format(_name, attributes, _name); 843 } 844 else 845 { 846 return "<%s%s />\r\n".format(_name, attributes); 847 } 848 } 849 850 /** 851 * Converts the node to a properly formatted dom node-string. 852 * Params: 853 * index = The tab index of the node. 854 * Returns: 855 * A string equivalent to the properly formatted dom node-string. 856 */ 857 string toString(size_t index) @trusted 858 { 859 import std..string : format; 860 import std.algorithm : map, filter; 861 import std.array : array, join; 862 863 string tabs = ""; 864 865 foreach (i; 0 .. index) 866 { 867 tabs ~= '\t'; 868 } 869 870 if (_textNode) 871 { 872 return _text ~ "\r\n"; 873 } 874 875 string attributes; 876 if (_attributes && _attributes.length) 877 { 878 attributes = " "; 879 880 attributes ~= _attributes.values.filter!(a => a.name && a.name.strip().length).map!(a => "%s=\"%s\"".format(a.name, a.value)).array.join(" "); 881 } 882 883 if (_children && _children.length) 884 { 885 string result = "%s<%s%s>\r\n".format(tabs, _name, attributes); 886 887 foreach (child; _children) 888 { 889 result ~= child.toString(index + 1); 890 } 891 892 result ~= "%s</%s>\r\n".format(tabs, _name); 893 894 return result; 895 } 896 else if (_text) 897 { 898 return "%s<%s%s>%s</%s>\r\n".format(tabs, _name, attributes, _text, _name); 899 } 900 else if (_parserSettings && _parserSettings.isSelfClosingTag(_name)) 901 { 902 return "%s<%s%s>\r\n".format(tabs, _name, attributes); 903 } 904 else if (_parserSettings && _parserSettings.isStandardTag(_name)) 905 { 906 return "%s<%s%s></%s>\r\n".format(tabs, _name, attributes, _name); 907 } 908 else 909 { 910 return "%s<%s%s />\r\n".format(tabs, _name, attributes); 911 } 912 } 913 914 /** 915 * Converts the node to a properly formatted dom node-string. 916 * Returns: 917 * A string equivalent to the properly formatted dom node-string. 918 */ 919 override string toString() @safe 920 { 921 return toString(0); 922 } 923 924 /// Operator overload. 925 override int opCmp(Object o) 926 { 927 auto node = cast(DomNode)o; 928 929 if (!node) 930 { 931 return -1; 932 } 933 934 if (node._nodeId == _nodeId) 935 { 936 return 0; 937 } 938 939 if (node._nodeId < _nodeId) 940 { 941 return -1; 942 } 943 944 return 1; 945 } 946 947 /// Operator overload. 948 override bool opEquals(Object o) 949 { 950 return opCmp(o) == 0; 951 } 952 }