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.css.css3selector;
7 
8 import std.uni : isWhite;
9 import std..string : format, strip, stripLeft, stripRight, toLower, indexOf;
10 
11 /// Wrapper around a css3 selection query.
12 final class Css3SelectionQuery
13 {
14   private:
15   /// The parent.
16   Css3SelectionQuery _parent;
17   /// The selector.
18   string _selector;
19   /// The selections.
20   Css3Selection[] _selections;
21   /// The next selection query.
22   Css3SelectionQuery _nextSelection;
23   /// The operator.
24   Css3SelectorOperator _operator;
25 
26   public:
27   final:
28   /// Creates a new css3 selection query.
29   this() @safe { }
30 
31   @property
32   {
33     /// Gets the parent.
34     Css3SelectionQuery parent() @safe { return _parent; }
35 
36     /// Gets the selections.
37     Css3Selection[] selections() @safe { return _selections; }
38 
39     /// Gets the next selection.
40     Css3SelectionQuery nextSelection() @safe { return _nextSelection; }
41 
42     /// Gets the operator.
43     Css3SelectorOperator operator() @safe { return _operator; }
44   }
45 }
46 
47 /// Enumeration around css3 selector operators.
48 enum Css3SelectorOperator
49 {
50   /// No selection.
51   none,
52   /// This is the ">" operator.
53   firstChild,
54   /// This is the "+" operator.
55   firstSibling,
56   /// This is a space.
57   allChildren,
58   /// This is the "~" operator.
59   allSiblings
60 }
61 
62 /// Wrapper around a css3 selection.
63 final class Css3Selection
64 {
65   private:
66   /// Boolean determining whether the selection has a wildcard or not.
67   bool _hasWildcard;
68   /// Tag names.
69   string[] _tagNames;
70   /// Ids.
71   string[] _ids;
72   /// Class names.
73   string[] _classNames;
74   /// States.
75   string[] _states;
76   /// The attribute selection.
77   Css3AttributeSelection _attributeSelection;
78   /// The attribute selector.
79   string _attributeSelector;
80 
81   public:
82   final:
83   /// Creates a new css3 selection.
84   this() @safe { }
85 
86   @property
87   {
88     /// Gets a boolean determining whether the selection hs a wildcard or not.
89     bool hasWildcard() @safe { return _hasWildcard; }
90 
91     /// Gets the tag names.
92     string[] tagNames() @safe { return _tagNames; }
93 
94     /// Gets the ids.
95     string[] ids() @safe { return _ids; }
96 
97     /// Gets the class names.
98     string[] classNames() @safe { return _classNames; }
99 
100     /// Gets the states.
101     string[] states() @safe { return _states; }
102 
103     /// Gets the attribute selection.
104     Css3AttributeSelection attributeSelection() @safe { return _attributeSelection; }
105   }
106 }
107 
108 /// Wrapper around a css3 attribute selection.
109 final class Css3AttributeSelection
110 {
111   private:
112   /// The name.
113   string _name;
114   /// The value.
115   string _value;
116   /// The attribute operator.
117   Css3SelectorAttributeOperator _operator;
118 
119   public:
120   final:
121   /// Creates a new css3 attribute selection.
122   this() @safe { }
123 
124   @property
125   {
126     /// Gets the name.
127     string name() @safe { return _name; }
128 
129     /// Gets the value.
130     string value() @safe { return _value; }
131 
132     /// Gets the operator.
133     Css3SelectorAttributeOperator operator() @safe { return _operator; }
134   }
135 }
136 
137 /// Enumeration o dom selector attribute operators.
138 enum Css3SelectorAttributeOperator
139 {
140   /// No operation.
141   none,
142   /// This is the "=" operator.
143   equals,
144   /// This is the "~=" operator.
145   containsWord,
146   /// This is the "|=" operator.
147   listStartsWith,
148   /// This is the "^=" operator.
149   startsWith,
150   /// This is the "$=" operator.
151   endsWith,
152   /// This is the "*=" operator.
153   contains
154 }
155 
156 /**
157 * Parses a css3 selector into a css3 selection query.
158 * Params:
159 *   selector = The selector to parse.
160 * Returns:
161 *   The css3 selection query.
162 */
163 Css3SelectionQuery parseCss3Selector(string selector) @safe
164 {
165   auto query = parseParts(selector);
166 
167   Css3SelectionQuery current = query;
168 
169   while (current)
170   {
171     current._selections ~= parsePart(current._selector);
172 
173     if (current._selections)
174     {
175       foreach (selection; current._selections)
176       {
177         selection._attributeSelection = parseAttribute(selection._attributeSelector);
178       }
179     }
180 
181     current = current._nextSelection;
182   }
183 
184   return query;
185 }
186 
187 private:
188 /**
189 * Parses the css3 parts.
190 * Params:
191 *   selector = The selector.
192 * Returns:
193 *   The css3 selection query with its parsed parts.
194 */
195 Css3SelectionQuery parseParts(string selector) @safe
196 {
197   if (!selector || !selector.length)
198   {
199     return null;
200   }
201 
202   selector = selector.strip();
203 
204   bool inAttribute;
205   Css3SelectionQuery root = new Css3SelectionQuery;
206   Css3SelectionQuery currentQuery = root;
207 
208   foreach (ref i; 0 .. selector.length)
209   {
210     char last = i > 0 ? selector[i - 1] : '\0';
211     char current = selector[i];
212     char next =  i < (selector.length - 1) ? selector[i + 1] : '\0';
213 
214     if (current == '[' && !inAttribute)
215     {
216       inAttribute = true;
217       currentQuery._selector ~= current;
218     }
219     else if (current == ']' && inAttribute)
220     {
221       inAttribute = false;
222       currentQuery._selector ~= current;
223 
224       if (i == (selector.length - 1) && !currentQuery._parent)
225       {
226         currentQuery._operator = Css3SelectorOperator.allChildren;
227         break;
228       }
229     }
230     else if (currentQuery._selector && currentQuery._selector.length && !inAttribute && (current == '>' || current == '+' || current == '~' || (current.isWhite && (next != '>' && next != '+' && next != '~'))))
231     {
232       auto temp = new Css3SelectionQuery;
233       temp._parent = currentQuery;
234 
235       switch (current)
236       {
237         case '>':
238           currentQuery._operator = Css3SelectorOperator.firstChild;
239 
240           currentQuery._nextSelection = temp;
241           currentQuery = temp;
242           break;
243 
244         case '+':
245           currentQuery._operator = Css3SelectorOperator.firstSibling;
246 
247           currentQuery._nextSelection = temp;
248           currentQuery = temp;
249           break;
250 
251         case '~':
252           currentQuery._operator = Css3SelectorOperator.allSiblings;
253 
254           currentQuery._nextSelection = temp;
255           currentQuery = temp;
256           break;
257 
258         default:
259         {
260           if (current.isWhite)
261           {
262             currentQuery._operator = Css3SelectorOperator.allChildren;
263 
264             currentQuery._nextSelection = temp;
265             currentQuery = temp;
266           }
267           break;
268         }
269       }
270     }
271     else if (!current.isWhite || inAttribute)
272     {
273       currentQuery._selector ~= current;
274 
275       if (i == (selector.length - 1) && !currentQuery._parent)
276       {
277         currentQuery._operator = Css3SelectorOperator.allChildren;
278         break;
279       }
280     }
281   }
282 
283   return root;
284 }
285 
286 /**
287 * Parses a selection part.
288 * Params:
289 *   selector = The selector part to parse.
290 * Returns:
291 *   An array of the part's selections.
292 */
293 Css3Selection[] parsePart(string selector) @safe
294 {
295   if (!selector || !selector.length)
296   {
297     return null;
298   }
299 
300   Css3Selection[] selections;
301 
302   auto selection = new Css3Selection;
303   string currentAttributeSelector;
304   string identifier;
305   string state;
306   bool isClass;
307   bool isId;
308   bool isState;
309 
310   void finalizeSelection() @safe
311   {
312     if (identifier && identifier.length && selection)
313     {
314       if (isClass)
315       {
316         selection._classNames ~= identifier;
317       }
318       else if (isId)
319       {
320         selection._ids ~= identifier;
321       }
322       else
323       {
324         selection._tagNames ~= identifier;
325       }
326     }
327 
328     if (state && state.length && selection)
329     {
330       selection._states ~= state;
331     }
332 
333     isState = false;
334     isClass = false;
335     isId = false;
336     identifier = null;
337     state = null;
338   }
339 
340   foreach (i; 0 .. selector.length)
341   {
342     char last = i > 0 ? selector[i - 1] : '\0';
343     char current = selector[i];
344     char next =  i < (selector.length - 1) ? selector[i + 1] : '\0';
345 
346     if (current == '[' && !currentAttributeSelector)
347     {
348       finalizeSelection();
349 
350       currentAttributeSelector = "";
351     }
352     else if (current == ']' && currentAttributeSelector)
353     {
354       selection._attributeSelector = currentAttributeSelector;
355       currentAttributeSelector = null;
356 
357       selections ~= selection;
358       selection = new Css3Selection;
359     }
360     else if (currentAttributeSelector)
361     {
362       currentAttributeSelector ~= current;
363     }
364     else if (current == '*')
365     {
366       selection._hasWildcard = true;
367       break;
368     }
369     else if (current == '.')
370     {
371       finalizeSelection();
372 
373       isClass = true;
374     }
375     else if (current == '#')
376     {
377       finalizeSelection();
378 
379       isId = true;
380     }
381     else if (current == ':')
382     {
383       isState = true;
384     }
385     else if (isState)
386     {
387       state ~= current;
388     }
389     else
390     {
391       identifier ~= current;
392     }
393   }
394 
395   if (!selection._hasWildcard)
396   {
397     finalizeSelection();
398   }
399 
400   if (selection._hasWildcard || selection._tagNames || selection._ids || selection._classNames || selection._attributeSelector || selection._states)
401   {
402     selections ~= selection;
403   }
404 
405   return selections;
406 }
407 
408 /**
409 * Parses an attribute selector.
410 * Params:
411 *   selector = The attribute selector to parse.
412 * Returns:
413 *   THe css3 attribute selection.
414 */
415 Css3AttributeSelection parseAttribute(string selector) @safe
416 {
417   auto attribute = new Css3AttributeSelection;
418   string name;
419   string value;
420 
421   foreach (i; 0 .. selector.length)
422   {
423     char last = i > 0 ? selector[i - 1] : '\0';
424     char current = selector[i];
425     char next =  i < (selector.length - 1) ? selector[i + 1] : '\0';
426 
427     if (attribute._operator)
428     {
429       value ~= current;
430     }
431     else if (current == '=')
432     {
433       switch (last)
434       {
435         case '~':
436           attribute._operator = Css3SelectorAttributeOperator.containsWord;
437           break;
438 
439         case '|':
440           attribute._operator = Css3SelectorAttributeOperator.listStartsWith;
441           break;
442 
443         case '^':
444           attribute._operator = Css3SelectorAttributeOperator.startsWith;
445           break;
446 
447         case '$':
448           attribute._operator = Css3SelectorAttributeOperator.endsWith;
449           break;
450 
451         case '*':
452           attribute._operator = Css3SelectorAttributeOperator.contains;
453           break;
454 
455         default:
456           attribute._operator = Css3SelectorAttributeOperator.equals;
457           break;
458       }
459     }
460     else if (current != '=' && current != '~' && current != '|' && current != '^' && current != '$' && current != '*')
461     {
462       name ~= current;
463     }
464   }
465 
466   if (name && name.length)
467   {
468     attribute._name = name.strip().stripLeft("'").stripRight("'").stripLeft("\"").stripRight("\"");
469   }
470 
471   if (value && value.length)
472   {
473     attribute._value = value.strip().stripLeft("'").stripRight("'").stripLeft("\"").stripRight("\"");
474   }
475 
476   return attribute;
477 }