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 }