Merge branch 'main/rendor-staging' into fixes
[ryzomcore.git] / nel / src / gui / css_parser.cpp
blobdcc82dfc65ca8f699d0bf5920af40a59b54c3e9a
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_parser.h"
25 #include "nel/gui/css_style.h"
26 #include "nel/gui/css_selector.h"
28 using namespace NLMISC;
30 #ifdef DEBUG_NEW
31 #define new DEBUG_NEW
32 #endif
34 namespace NLGUI
36 // ***************************************************************************
37 // Parse style declarations style, eg. "color:red; font-size: 10px;"
39 // key is converted to lowercase
40 // value is left as is
41 TStyleVec CCssParser::parseDecls(const std::string &styleString)
43 TStyleVec styles;
44 size_t pos = 0;
45 size_t end = styleString.size();
46 while(pos < end)
48 size_t sep = styleString.find(':', pos);
49 if (sep == std::string::npos)
50 break;
52 size_t keyIndex = pos;
53 size_t keyLength = sep - pos;
55 sep++;
56 pos = sep;
57 while(sep < end)
59 sep = styleString.find_first_of(";'\"(", sep);
60 if (sep == std::string::npos || styleString[sep] == ';')
61 break;
63 if (styleString[sep] == '\'' || styleString[sep] == '"')
65 char ch = styleString[sep];
66 // skip open quote
67 sep++;
68 while(sep < end && styleString[sep] != ch)
70 if (styleString[sep] == '\\')
71 sep++;
72 sep++;
74 // skip close quote
75 sep++;
77 else if (styleString[sep] == '(')
79 while(sep < end && styleString[sep] != ')')
81 sep++;
83 // skip close parenthesis
84 sep++;
88 styles.push_back(TStylePair(trim(styleString.substr(keyIndex, keyLength)), trim(styleString.substr(pos, sep - pos))));
90 if (sep >= end)
91 break;
93 pos = sep + 1;
95 return styles;
98 // ***************************************************************************
99 // Parse stylesheet, eg content from main.css file
101 // Return all found rules
102 void CCssParser::parseStylesheet(const std::string &cssString, std::vector<CCssStyle::SStyleRule> &result)
104 _Rules.clear();
105 _Style.clear();
107 _Style = cssString;
108 preprocess();
110 _Position = 0;
111 while(!is_eof())
113 skipWhitespace();
115 if (_Style[_Position] == '@')
116 readAtRule();
117 else
118 readRule();
121 result.insert(result.end(), _Rules.begin(), _Rules.end());
122 _Rules.clear();
125 // ***************************************************************************
126 // Parse selector with style string
127 // selector: "a#id .class"
128 // style: "color: red; font-size: 10px;"
130 // @internal
131 void CCssParser::parseRule(const std::string &selectorString, const std::string &styleString)
133 TStyleVec props;
134 props = parseDecls(styleString);
136 // duplicate props to each selector in selector list,
137 // example 'div > p, h1' creates 'div>p' and 'h1'
138 for(std::string::size_type pos = 0; pos < selectorString.size(); pos++)
140 while(pos < selectorString.size() && is_whitespace(selectorString[pos]))
141 pos++;
143 CCssStyle::SStyleRule rule;
144 rule.Selector = parse_selector(selectorString, rule.PseudoElement, pos);
145 rule.Properties = props;
146 if (!rule.Selector.empty())
147 _Rules.push_back(rule);
152 // ***************************************************************************
153 // Skip over at-rule
154 // @import ... ;
155 // @charset ... ;
156 // @media query { .. }
158 // @internal
159 void CCssParser::readAtRule()
161 // skip '@'
162 _Position++;
164 // skip 'import', 'media', etc
165 skipIdentifier();
167 // skip at-rule statement
168 while(!is_eof() && _Style[_Position] != ';')
170 if (maybe_escape())
172 escape();
174 else if (is_quote(_Style[_Position]))
176 skipString();
178 else if (is_block_open(_Style[_Position]))
180 bool mustBreak = (_Style[_Position] == '{');
181 skipBlock();
183 if(mustBreak)
185 break;
188 else
190 _Position++;
194 // skip ';' or '}'
195 _Position++;
198 // ***************************************************************************
199 // skip over "elm#id.selector[attr]:peseudo, .sel2 { rule }" block
200 // @internal
201 void CCssParser::readRule()
203 size_t start;
205 // selector
206 start = _Position;
207 while(!is_eof())
209 if (maybe_escape())
210 _Position++;
211 else if (is_quote(_Style[_Position]))
212 skipString();
213 else if (_Style[_Position] == '[')
214 skipBlock();
215 else if (_Style[_Position] == '{')
216 break;
217 else
218 _Position++;
221 if (!is_eof())
223 std::string selector;
224 selector.append(_Style, start, _Position - start);
226 skipWhitespace();
228 // declaration block
229 start = _Position;
230 skipBlock();
231 if (_Position <= _Style.size())
233 std::string rules;
234 rules.append(_Style, start + 1, _Position - start - 2);
236 parseRule(selector, rules);
241 // ***************************************************************************
242 // skip over \abcdef escaped sequence or escaped newline char
243 // @internal
244 void CCssParser::escape()
246 // skip '\'
247 _Position++;
248 if (is_hex(_Style[_Position]))
250 // TODO: '\abc def' should be considered one string
251 for(uint i=0; i<6 && is_hex(_Style[_Position]); i++)
252 _Position++;
254 if (_Style[_Position] == ' ')
255 _Position++;
257 else if (_Style[_Position] != 0x0A)
258 _Position++;
261 // ***************************************************************************
262 // @internal
263 bool CCssParser::skipIdentifier()
265 size_t start = _Position;
266 bool valid = true;
267 while(!is_eof() && valid)
269 if (maybe_escape())
271 escape();
272 continue;
274 else if (is_alpha(_Style[_Position]))
276 // valid
278 else if (is_digit(_Style[_Position]))
280 if (_Position == start)
282 // cannot start with digit
283 valid = false;
285 else if ((_Position - start) == 0 && _Style[_Position-1] == '-')
287 // cannot start with -#
288 valid = false;
291 else if (_Style[_Position] == '_')
293 // valid
295 else if (_Style[_Position] >= 0x80)
297 // valid
299 else if (_Style[_Position] == '-')
301 if ((_Position - start) == 1 && _Style[_Position-1] == '-')
303 // cannot start with --
304 valid = false;
307 else
309 // we're done
310 break;
313 _Position++;
316 return valid && !is_eof();
319 // ***************************************************************************
320 // skip over (..), [..], or {..} blocks
321 // @internal
322 void CCssParser::skipBlock()
324 char startChar = _Style[_Position];
326 // block start
327 _Position++;
328 while(!is_eof() && !is_block_close(_Style[_Position], startChar))
330 if (maybe_escape())
331 // skip backslash and next char
332 _Position += 2;
333 else if (is_quote(_Style[_Position]))
334 skipString();
335 else if (is_block_open(_Style[_Position]))
336 skipBlock();
337 else
338 _Position++;
341 // block end
342 _Position++;
345 // ***************************************************************************
346 // skip over quoted string
347 // @internal
348 void CCssParser::skipString()
350 char endChar = _Style[_Position];
352 // quote start
353 _Position++;
354 while(!is_eof() && _Style[_Position] != endChar)
356 if (maybe_escape())
357 _Position++;
359 _Position++;
362 // quote end
363 _Position++;
366 // ***************************************************************************
367 // @internal
368 void CCssParser::skipWhitespace()
370 while(!is_eof() && is_whitespace(_Style[_Position]))
371 _Position++;
374 // ***************************************************************************
375 // parse selector list
376 // @internal
377 std::vector<CCssSelector> CCssParser::parse_selector(const std::string &sel, std::string &pseudoElement, std::string::size_type &pos) const
379 std::vector<CCssSelector> result;
380 CCssSelector current;
382 pseudoElement.clear();
384 bool failed = false;
385 std::string::size_type start = pos;
386 while(pos < sel.size() && sel[pos] != ',')
388 std::string uc;
389 uc = sel[pos];
390 if (is_nmchar(sel[pos]) && current.empty())
392 pos++;
394 while(pos < sel.size() && is_nmchar(sel[pos]))
395 pos++;
397 current.Element = toLowerAscii(sel.substr(start, pos - start));
398 start = pos;
399 continue;
402 if (sel[pos] == '*' && current.empty())
404 pos++;
405 current.Element = "*";
406 start = pos;
407 continue;
410 if(sel[pos] == '#')
412 pos++;
413 start=pos;
415 while(pos < sel.size() && is_nmchar(sel[pos]))
416 pos++;
418 current.Id = toLowerAscii(sel.substr(start, pos - start));
419 start = pos;
421 else if (sel[pos] == '.')
423 pos++;
424 start=pos;
426 // .classA.classB
427 while(pos < sel.size() && (is_nmchar(sel[pos]) || sel[pos] == '.'))
428 pos++;
430 current.setClass(toLowerAscii(sel.substr(start, pos - start)));
431 start = pos;
433 else if (sel[pos] == '[')
435 pos++;
436 start = pos;
438 if (is_whitespace(sel[pos]))
440 while(pos < sel.size() && is_whitespace(sel[pos]))
441 pos++;
443 start = pos;
446 std::string key;
447 std::string value;
448 char op = ' ';
450 // key
451 while(pos < sel.size() && is_nmchar(sel[pos]))
452 pos++;
454 key = sel.substr(start, pos - start);
455 if (pos == sel.size()) break;
457 if (is_whitespace(sel[pos]))
459 while(pos < sel.size() && is_whitespace(sel[pos]))
460 pos++;
462 if (pos == sel.size()) break;
465 if (sel[pos] == ']')
467 current.addAttribute(key);
469 else
471 // operand
472 op = sel[pos];
473 if (op == '~' || op == '|' || op == '^' || op == '$' || op == '*')
475 pos++;
476 if (pos == sel.size()) break;
479 // invalid rule?, eg [attr^value]
480 if (sel[pos] != '=')
482 while(pos < sel.size() && sel[pos] != ']')
483 pos++;
485 if (pos == sel.size()) break;
487 start = pos;
489 else
491 // skip '='
492 pos++;
494 if (is_whitespace(sel[pos]))
496 while(pos < sel.size() && is_whitespace(sel[pos]))
497 pos++;
499 if (pos == sel.size()) break;
502 // value
503 start = pos;
504 bool quote = false;
505 char quoteOpen;
506 while(pos < sel.size())
508 if (sel[pos] == '\'' || sel[pos] == '"')
510 // skip over quoted value
511 start = pos;
512 pos++;
513 while(pos < sel.size() && sel[pos] != sel[start])
515 if (sel[pos] == '\\')
517 pos++;
519 pos++;
522 if (pos == sel.size()) break;
524 else if (sel[pos] == '\\')
526 pos++;
528 else if (!quote && sel[pos] == ']')
530 value = sel.substr(start, pos - start);
531 break;
534 pos++;
535 } // while 'value'
537 if (pos == sel.size()) break;
539 bool cs = true;
540 // [value="attr" i]
541 if (value.size() > 2 && value[value.size()-2] == ' ')
543 char lastChar = value[value.size()-1];
544 if (lastChar == 'i' || lastChar == 'I' || lastChar == 's' || lastChar == 'S')
546 value = value.substr(0, value.size()-2);
547 cs = !((lastChar == 'i' || lastChar == 'I'));
550 current.addAttribute(key, trimQuotes(value), (char)op, cs);
551 } // op error
552 } // no value
554 // skip ']'
555 pos++;
557 start = pos;
559 else if (sel[pos] == ':')
561 pos++;
562 start=pos;
563 // pseudo element, eg '::before'
564 if (pos < sel.size() && sel[pos] == ':')
566 pos++;
568 // :first-child
569 // :nth-child(2n+0)
570 // :not(h1, div#main)
571 // :not(:nth-child(2n+0))
572 // has no support for quotes, eg :not(h1[attr=")"]) fails
573 while(pos < sel.size() && (is_nmchar(sel[pos]) || sel[pos] == '('))
575 if (sel[pos] == '(')
577 uint open = 1;
578 pos++;
579 while(pos < sel.size() && open > 0)
581 if (sel[pos] == ')')
582 open--;
583 else if (sel[pos] == '(')
584 open++;
586 pos++;
588 break;
590 else
592 pos++;
596 std::string key = toLowerAscii(sel.substr(start, pos - start));
597 if (key.empty())
599 failed = true;
600 break;
603 if (key[0] == ':' || key == "after" || key == "before" || key == "cue" || key == "first-letter" || key == "first-line")
605 if (!pseudoElement.empty())
607 failed = true;
608 break;
610 if (key[0] != ':')
612 pseudoElement = ":" + key;
614 else
616 pseudoElement = key;
619 else
621 current.addPseudoClass(key);
624 start = pos;
626 else if (!current.empty())
628 // pseudo element like ':before' can only be set on the last selector
629 // user action pseudo classes can be used after pseudo element (ie, :focus, :hover)
630 // there is no support for those and its safe to just fail the selector
631 if (!result.empty() && !pseudoElement.empty())
633 failed = true;
634 break;
637 // start new selector as combinator is part of next selector
638 result.push_back(current);
639 current = CCssSelector();
641 // detect and remove whitespace around combinator, eg ' > '
642 bool isSpace = is_whitespace(sel[pos]);;
643 while(pos < sel.size() && is_whitespace(sel[pos]))
644 pos++;
646 if (sel[pos] == '>' || sel[pos] == '+' || sel[pos] == '~')
648 current.Combinator = sel[pos];
649 pos++;
651 while(pos < sel.size() && is_whitespace(sel[pos]))
652 pos++;
654 else if (isSpace)
656 if (sel[pos] != ',' && sel[pos] != '\0')
657 current.Combinator = ' ';
659 else
661 // unknown
662 current.Combinator = sel[pos];
663 pos++;
666 start = pos;
668 else
670 pos++;
674 if (failed)
676 result.clear();
678 else if (!current.empty())
680 result.push_back(current);
683 return result;
686 // ***************************************************************************
687 // @internal
688 void CCssParser::preprocess()
690 _Position = 0;
692 size_t start;
693 size_t charsLeft;
694 bool quote = false;
695 char quoteChar;
696 while(!is_eof())
698 charsLeft = _Style.size() - _Position - 1;
700 // FF, CR
701 if (_Style[_Position] == 0x0C || _Style[_Position] == 0x0D)
703 uint len = 1;
704 // CR, LF
705 if (charsLeft >= 1 && _Style[_Position] == 0x0D && _Style[_Position+1] == 0x0A)
706 len++;
708 std::string tmp;
709 tmp += 0x0A;
710 _Style.replace(_Position, 1, tmp);
712 else if (_Style[_Position] == 0x00)
714 // Unicode replacement character
715 // _Style[_Position] = 0xFFFD;
716 _Style.replace(_Position, 1, "\xEF\xBF\xBD");
718 else
720 // strip comments for easier parsing
721 if (_Style[_Position] == '\\')
723 _Position++;
725 else if (is_quote(_Style[_Position]))
727 if (!quote)
728 quoteChar = _Style[_Position];
730 if (quote && _Style[_Position] == quoteChar)
731 quote = !quote;
733 else if (!quote && is_comment_open())
735 size_t pos = _Style.find("*/", _Position + 2);
736 if (pos == std::string::npos)
737 pos = _Style.size();
739 _Style.erase(_Position, pos - _Position + 2);
740 std::string uc;
741 uc = _Style[_Position];
743 // _Position is already at correct place
744 continue;
748 _Position++;
751 } // namespace