Merge branch 'fixes' into main/rendor-staging
[ryzomcore.git] / nel / src / gui / css_selector.cpp
blob16b7cec33395953223576a4bf6f2a16e903136b5
1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010-2021 Winch Gate Property Limited
3 //
4 // This source file has been modified by the following contributors:
5 // Copyright (C) 2020 Jan BOON (Kaetemi) <jan.boon@kaetemi.be>
6 //
7 // This program is free software: you can redistribute it and/or modify
8 // it under the terms of the GNU Affero General Public License as
9 // published by the Free Software Foundation, either version 3 of the
10 // License, or (at your option) any later version.
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 // GNU Affero General Public License for more details.
17 // You should have received a copy of the GNU Affero General Public License
18 // along with this program. If not, see <http://www.gnu.org/licenses/>.
20 #include "stdpch.h"
22 #include <string>
23 #include "nel/misc/types_nl.h"
24 #include "nel/gui/css_selector.h"
25 #include "nel/gui/html_element.h"
27 using namespace NLMISC;
29 #ifdef DEBUG_NEW
30 #define new DEBUG_NEW
31 #endif
33 namespace NLGUI
35 CCssSelector::CCssSelector(std::string elm, std::string id, std::string cls, char comb)
36 : Element(elm), Id(id), Class(), Attr(), PseudoClass(), Combinator(comb)
38 if (!cls.empty())
40 setClass(cls);
44 uint32 CCssSelector::specificity() const
46 uint ret = 0;
48 if (!Element.empty() && Element != "*") ret += 0x000001;
49 // Pseudo Element is added in CCssStyle
50 //if (!PseudoElement.empty()) ret += 0x000001;
52 if (!Class.empty()) ret += 0x000100 * Class.size();
53 if (!Attr.empty()) ret += 0x000100 * Attr.size();
54 // TODO: has different cases
55 if (!PseudoClass.empty()) ret += 0x000100 * PseudoClass.size();
57 if (!Id.empty()) ret += 0x010000;
59 return ret;
62 void CCssSelector::setClass(const std::string &cls)
64 std::vector<std::string> parts;
65 NLMISC::splitString(toLowerAscii(cls), ".", parts);
67 for(uint i = 0; i< parts.size(); i++)
69 std::string cname = trim(parts[i]);
70 if (!cname.empty())
72 Class.push_back(cname);
77 void CCssSelector::addAttribute(const std::string &key, const std::string &val, char op, bool cs)
79 if (cs)
81 // case sensitive match
82 Attr.push_back(SAttribute(key, val, op, cs));
84 else
86 Attr.push_back(SAttribute(key, toLowerAscii(val), op, cs));
90 void CCssSelector::addPseudoClass(const std::string &key)
92 if (key.empty()) return;
94 PseudoClass.push_back(key);
97 bool CCssSelector::match(const CHtmlElement &elm) const
99 if (!Element.empty() && Element != "*" && Element != elm.Value)
101 return false;
104 if (!Id.empty() && Id != elm.getAttribute("id"))
106 return false;
109 if (!Class.empty() && !matchClass(elm))
111 return false;
114 if (!Attr.empty() && !matchAttributes(elm))
116 return false;
119 if (!PseudoClass.empty() && !matchPseudoClass(elm))
121 return false;
124 return true;
127 bool CCssSelector::matchClass(const CHtmlElement &elm) const
129 // make sure all class names we have, other has as well
130 for(uint i = 0; i< Class.size(); ++i)
132 if (!elm.hasClass(Class[i]))
134 return false;
138 return true;
141 bool CCssSelector::matchAttributes(const CHtmlElement &elm) const
143 // TODO: refactor into matchAttributeSelector
144 for(uint i = 0; i< Attr.size(); ++i)
146 if (!elm.hasAttribute(Attr[i].key)) return false;
148 std::string value = elm.getAttribute(Attr[i].key);
149 // case-insensitive compare, Attr.value is already lowercased
150 if (!Attr[i].caseSensitive)
152 value = toLowerAscii(value);
155 switch(Attr[i].op)
157 case '=':
158 if (Attr[i].value != value) return false;
159 break;
160 case '~':
162 // exact match to any of whitespace separated words from element attribute
163 if (Attr[i].value.empty()) return false;
165 std::vector<std::string> parts;
166 NLMISC::splitString(value, " ", parts);
167 bool found = false;
168 for(uint j = 0; j < parts.size(); j++)
170 if (Attr[i].value == parts[j])
172 found = true;
173 break;
176 if (!found) return false;
178 break;
179 case '|':
180 // exact value, or start with val+'-'
181 if (value != Attr[i].value && value.find(Attr[i].value + "-") == std::string::npos) return false;
182 break;
183 case '^':
184 // prefix, starts with
185 if (Attr[i].value.empty()) return false;
186 if (value.find(Attr[i].value) != 0) return false;
187 break;
188 case '$':
189 // suffic, ends with
190 if (Attr[i].value.empty() || value.size() < Attr[i].value.size()) return false;
191 if (Attr[i].value == value.substr(value.size() - Attr[i].value.size())) return false;
192 break;
193 case '*':
194 if (Attr[i].value.empty()) return false;
195 if (value.find(Attr[i].value) == std::string::npos) return false;
196 break;
197 case ' ':
198 // contains key, ignore value
199 break;
200 default:
201 // unknown comparison
202 return false;
206 return true;
209 bool CCssSelector::matchPseudoClass(const CHtmlElement &elm) const
211 for(uint i = 0; i< PseudoClass.size(); i++)
213 if (PseudoClass[i] == "root")
215 // ':root' is just 'html' with higher specificity
216 if (elm.Value != "html") return false;
218 else if (PseudoClass[i] == "only-child")
220 if (elm.parent && !elm.parent->Children.empty()) return false;
222 else
224 if (PseudoClass[i] == "first-child")
226 if (elm.previousSibling) return false;
228 else if (PseudoClass[i] == "last-child")
230 if (elm.nextSibling) return false;
232 else if (PseudoClass[i].find("nth-child(") != std::string::npos)
234 sint a, b;
235 // TODO: there might be multiple :nth-child() on single selector, so current can't cache it
236 parseNth(PseudoClass[i], a, b);
238 // 1st child should be '1' and not '0'
239 if (!matchNth(elm.childIndex+1, a, b)) return false;
241 else if (startsWith(PseudoClass[i], "lang("))
243 std::string lang = PseudoClass[i].substr(5, PseudoClass[i].size() - 6);
244 if (lang.empty() || !matchLang(elm, lang)) return false;
246 else
248 return false;
253 return true;
256 void CCssSelector::parseNth(const std::string &pseudo, sint &a, sint &b) const
258 a = 0;
259 b = 0;
261 std::string::size_type start = pseudo.find_first_of("(") + 1;
262 std::string::size_type end = pseudo.find_first_of(")");
264 if (start == std::string::npos) return;
266 std::string expr = toLowerAscii(pseudo.substr(start, end - start));
268 if (expr.empty()) return;
270 if (expr == "even")
272 // 2n+0
273 a = 2;
274 b = 0;
276 else if (expr == "odd")
278 // 2n+1
279 a = 2;
280 b = 1;
282 else
284 // -An+B, An+B, An-B
285 std::string::size_type pos;
287 start = 0;
288 pos = expr.find_first_of("n", start);
289 if (pos == std::string::npos)
291 fromString(expr, b);
293 else if (pos == 0)
295 // 'n' == '1n'
296 a = 1;
298 else if (expr[0] == '-' && pos == 1)
300 // '-n' == '-1n'
301 a = -1;
303 else
305 fromString(expr.substr(start, pos - start), a);
308 start = pos;
309 pos = expr.find_first_of("+-", start);
310 if (pos != std::string::npos && (expr[pos] == '+' || expr[pos] == '-'))
312 // copy with sign char
313 fromString(expr.substr(pos, end - pos), b);
318 bool CCssSelector::matchNth(sint childNr, sint a, sint b) const
320 if (a == 0)
322 return childNr == b;
324 else if (a > 0)
326 return childNr >= b && (childNr - b) % a == 0;
328 else
330 // a is negative from '-An+B'
331 return childNr <= b && (b - childNr) % (-a) == 0;
335 bool CCssSelector::matchLang(const CHtmlElement &elm, const std::string &pseudo) const
337 // TODO: does not support comma separated, or escaped/quoted/wildcard tags
338 std::string lang = toLowerAscii(elm.getInheritedLanguage());
339 if (lang.empty() || pseudo.empty())
340 return false;
342 // lang = 'en', pseudo = 'en-US'
343 if (lang.size() < pseudo.size())
344 return false;
346 std::string selector = toLowerAscii(pseudo);
347 bool selectorHasRegion = selector.find("-") != std::string::npos;
348 bool langHasRegion = lang.find("-") != std::string::npos;
350 // both are 'en', or 'en-US' type
351 if (langHasRegion == selectorHasRegion)
352 return lang == selector;
354 // lang = 'en-US', selector = 'en'
355 return lang[selector.size()] == '-' && startsWith(lang, selector);
358 std::string CCssSelector::toString() const
360 std::string ret;
361 ret += Element;
362 ret += Id;
363 if (!Class.empty())
365 for(uint i = 0; i<Class.size(); i++)
366 ret += "." + Class[i];
368 if (!Attr.empty())
370 for(uint i = 0; i<Attr.size(); ++i)
372 ret += "[" + Attr[i].key;
373 if (Attr[i].op != ' ')
375 ret += Attr[i].op + Attr[i].value;
377 ret += "]";
380 if (!PseudoClass.empty())
382 for(uint i = 0; i<PseudoClass.size(); ++i)
384 ret += ":" + PseudoClass[i];
387 if (Combinator != '\0')
389 ret += Combinator;
392 // ret += ":" + PseudoClass;
393 return ret;
396 } // namespace