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 }