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.xhtml.xhtmldocument;
7 
8 import diamond.xhtml.xhtmlexception;
9 import diamond.dom.domdocument;
10 import diamond.dom.domnode;
11 import diamond.dom.domparsersettings;
12 import diamond.xhtml.xhtmlnode;
13 
14 /// An XHTML document.
15 final class XHtmlDocument : DomDocument
16 {
17   private:
18   /// The root nodes.
19   XHtmlNode[] _rootNodes;
20   /// The doctype node.
21   XHtmlNode _doctype;
22   /// The head node.
23   XHtmlNode _head;
24   /// The body node.
25   XHtmlNode _body;
26 
27   public:
28   final:
29   /**
30   * Creates a new xhtml document.
31   * Params:
32   *   parserSettings = The settings used for parsing the document.
33   */
34   this(DomParserSettings parserSettings) @safe
35   {
36     super(parserSettings);
37   }
38 
39   /**
40   * Parses the elements from the dom to the document.
41   * Params:
42   *   elements = The parsed dom elements.
43   */
44   override void parseElements(DomNode[] elements) @safe
45   {
46     if (!elements)
47     {
48       return;
49     }
50 
51     foreach (element; elements)
52     {
53       import std..string : toLower;
54 
55       if (element.name.toLower() == "doctype")
56       {
57         _doctype = element;
58       }
59       else
60       {
61         if (element.name.toLower() == "head")
62         {
63           _head = element;
64         }
65         else if (element.name.toLower() == "body")
66         {
67           _body = element;
68         }
69         else
70         {
71           if (element.name.toLower() == "html")
72           {
73             if (element.children)
74             {
75               foreach (child; element.children)
76               {
77                 if (child.name.toLower() == "head")
78                 {
79                   _head = child;
80                 }
81                 else if (child.name.toLower() == "body")
82                 {
83                   _body = child;
84                 }
85               }
86             }
87           }
88         }
89 
90         _rootNodes ~= element;
91       }
92     }
93   }
94 
95   @property
96   {
97     /// Gets the root nodes of the html document.
98     XHtmlNode[] rootNodes() @safe { return _rootNodes; }
99 
100     /// Sets the root nodes of the html document.
101     void root(XHtmlNode[] nodes) @safe
102     {
103       _rootNodes = nodes;
104 
105       if (!_rootNodes)
106       {
107         return;
108       }
109 
110       foreach (element; _rootNodes)
111       {
112         import std..string : toLower;
113 
114         if (element.name.toLower() == "doctype")
115         {
116           _doctype = element;
117         }
118         else if (element.name.toLower() == "head")
119         {
120           _head = element;
121         }
122         else if (element.name.toLower() == "body")
123         {
124           _body = element;
125         }
126         else if (element.name.toLower() == "html")
127         {
128           if (element.children)
129           {
130             foreach (child; element.children)
131             {
132               if (child.name.toLower() == "head")
133               {
134                 _head = child;
135               }
136               else if (child.name.toLower() == "body")
137               {
138                 _body = child;
139               }
140             }
141           }
142         }
143       }
144     }
145 
146     /// Gets the head node.
147     XHtmlNode head() @safe { return _head; }
148 
149     /// Gets the body node.
150     XHtmlNode body() @safe { return _body; }
151   }
152 
153   /**
154   * Queries all dom nodes based on a css3 selector.
155   * Params:
156   *   selector = The css3 selector.
157   * Returns:
158   *   An array of all matching nodes.
159   */
160   XHtmlNode[] querySelectorAll(string selector)
161   {
162     import std.array : array;
163     import std.algorithm : map, filter, sort, group;
164 
165     XHtmlNode[] elements;
166 
167     auto dummyNode = new XHtmlNode(null);
168 
169     foreach (rootNode; _rootNodes)
170     {
171       dummyNode.addChild(rootNode);
172 
173       elements ~= dummyNode.querySelectorAll(selector);
174     }
175 
176     return elements ? elements.sort.group.map!(g => g[0]).array : [];
177   }
178 
179   /**
180   * Queries the first dom node based on a css3 selector.
181   * Params:
182   *   selector = The css3 selector.
183   * Returns:
184   *   The node if found, null otherwise.
185   */
186   XHtmlNode querySelector(string selector)
187   {
188     auto result = querySelectorAll(selector);
189 
190     if (!result || !result.length)
191     {
192       return null;
193     }
194 
195     return result[0];
196   }
197 
198   /**
199   * Gets a dom node by an attribute named "id" matching the given value.
200   * Params:
201   *   id = The id of the node to retrieve.
202   * Returns:
203   *   The dom node if found, null otherwise.
204   */
205   XHtmlNode getElementById(string id) @safe
206   {
207     foreach (rootNode; _rootNodes)
208     {
209       if (rootNode.hasAttribute("id", id))
210       {
211         return rootNode;
212       }
213 
214       auto element = rootNode.getElementById(id);
215 
216       if (element)
217       {
218         return element;
219       }
220     }
221 
222     return null;
223   }
224 
225   /// XHtml documents cannot be repaired. Use HtmlDocument.repairDocument() instead.
226   override void repairDocument() @safe
227   {
228     throw new XHtmlException("Cannot repair XHtml documents, because they're strict html. Use HtmlDocument.repairDocument() instead.");
229   }
230 
231   /**
232   * Converts the xhtml document to a properly formatted xhtml document-string.
233   * Returns:
234   *   A string equivalent to the properly formatted xhtml document-string.
235   */
236   override string toString() @safe
237   {
238     import std.array : join, array;
239     import std.algorithm : map;
240     import std..string : format;
241 
242     return (_doctype ? "<!%s %s>\r\n".format(_doctype.name, _doctype.getAttributes().map!(a => a.value ? "%s=\"%s\"".format(a.name, a.value) : a.name).array.join(" ")) : "") ~ (_rootNodes ? join(_rootNodes.map!(n => n.toString).array, "\r\n") : "");
243   }
244 }