1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010-2021 Winch Gate Property Limited
4 // This source file has been modified by the following contributors:
5 // Copyright (C) 2020 Jan BOON (Kaetemi) <jan.boon@kaetemi.be>
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/>.
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
;
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
)
45 size_t end
= styleString
.size();
48 size_t sep
= styleString
.find(':', pos
);
49 if (sep
== std::string::npos
)
52 size_t keyIndex
= pos
;
53 size_t keyLength
= sep
- pos
;
59 sep
= styleString
.find_first_of(";'\"(", sep
);
60 if (sep
== std::string::npos
|| styleString
[sep
] == ';')
63 if (styleString
[sep
] == '\'' || styleString
[sep
] == '"')
65 char ch
= styleString
[sep
];
68 while(sep
< end
&& styleString
[sep
] != ch
)
70 if (styleString
[sep
] == '\\')
77 else if (styleString
[sep
] == '(')
79 while(sep
< end
&& styleString
[sep
] != ')')
83 // skip close parenthesis
88 styles
.push_back(TStylePair(trim(styleString
.substr(keyIndex
, keyLength
)), trim(styleString
.substr(pos
, sep
- pos
))));
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
)
115 if (_Style
[_Position
] == '@')
121 result
.insert(result
.end(), _Rules
.begin(), _Rules
.end());
125 // ***************************************************************************
126 // Parse selector with style string
127 // selector: "a#id .class"
128 // style: "color: red; font-size: 10px;"
131 void CCssParser::parseRule(const std::string
&selectorString
, const std::string
&styleString
)
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
]))
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 // ***************************************************************************
156 // @media query { .. }
159 void CCssParser::readAtRule()
164 // skip 'import', 'media', etc
167 // skip at-rule statement
168 while(!is_eof() && _Style
[_Position
] != ';')
174 else if (is_quote(_Style
[_Position
]))
178 else if (is_block_open(_Style
[_Position
]))
180 bool mustBreak
= (_Style
[_Position
] == '{');
198 // ***************************************************************************
199 // skip over "elm#id.selector[attr]:peseudo, .sel2 { rule }" block
201 void CCssParser::readRule()
211 else if (is_quote(_Style
[_Position
]))
213 else if (_Style
[_Position
] == '[')
215 else if (_Style
[_Position
] == '{')
223 std::string selector
;
224 selector
.append(_Style
, start
, _Position
- start
);
231 if (_Position
<= _Style
.size())
234 rules
.append(_Style
, start
+ 1, _Position
- start
- 2);
236 parseRule(selector
, rules
);
241 // ***************************************************************************
242 // skip over \abcdef escaped sequence or escaped newline char
244 void CCssParser::escape()
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
++)
254 if (_Style
[_Position
] == ' ')
257 else if (_Style
[_Position
] != 0x0A)
261 // ***************************************************************************
263 bool CCssParser::skipIdentifier()
265 size_t start
= _Position
;
267 while(!is_eof() && valid
)
274 else if (is_alpha(_Style
[_Position
]))
278 else if (is_digit(_Style
[_Position
]))
280 if (_Position
== start
)
282 // cannot start with digit
285 else if ((_Position
- start
) == 0 && _Style
[_Position
-1] == '-')
287 // cannot start with -#
291 else if (_Style
[_Position
] == '_')
295 else if (_Style
[_Position
] >= 0x80)
299 else if (_Style
[_Position
] == '-')
301 if ((_Position
- start
) == 1 && _Style
[_Position
-1] == '-')
303 // cannot start with --
316 return valid
&& !is_eof();
319 // ***************************************************************************
320 // skip over (..), [..], or {..} blocks
322 void CCssParser::skipBlock()
324 char startChar
= _Style
[_Position
];
328 while(!is_eof() && !is_block_close(_Style
[_Position
], startChar
))
331 // skip backslash and next char
333 else if (is_quote(_Style
[_Position
]))
335 else if (is_block_open(_Style
[_Position
]))
345 // ***************************************************************************
346 // skip over quoted string
348 void CCssParser::skipString()
350 char endChar
= _Style
[_Position
];
354 while(!is_eof() && _Style
[_Position
] != endChar
)
366 // ***************************************************************************
368 void CCssParser::skipWhitespace()
370 while(!is_eof() && is_whitespace(_Style
[_Position
]))
374 // ***************************************************************************
375 // parse selector list
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();
385 std::string::size_type start
= pos
;
386 while(pos
< sel
.size() && sel
[pos
] != ',')
390 if (is_nmchar(sel
[pos
]) && current
.empty())
394 while(pos
< sel
.size() && is_nmchar(sel
[pos
]))
397 current
.Element
= toLowerAscii(sel
.substr(start
, pos
- start
));
402 if (sel
[pos
] == '*' && current
.empty())
405 current
.Element
= "*";
415 while(pos
< sel
.size() && is_nmchar(sel
[pos
]))
418 current
.Id
= toLowerAscii(sel
.substr(start
, pos
- start
));
421 else if (sel
[pos
] == '.')
427 while(pos
< sel
.size() && (is_nmchar(sel
[pos
]) || sel
[pos
] == '.'))
430 current
.setClass(toLowerAscii(sel
.substr(start
, pos
- start
)));
433 else if (sel
[pos
] == '[')
438 if (is_whitespace(sel
[pos
]))
440 while(pos
< sel
.size() && is_whitespace(sel
[pos
]))
451 while(pos
< sel
.size() && is_nmchar(sel
[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
]))
462 if (pos
== sel
.size()) break;
467 current
.addAttribute(key
);
473 if (op
== '~' || op
== '|' || op
== '^' || op
== '$' || op
== '*')
476 if (pos
== sel
.size()) break;
479 // invalid rule?, eg [attr^value]
482 while(pos
< sel
.size() && sel
[pos
] != ']')
485 if (pos
== sel
.size()) break;
494 if (is_whitespace(sel
[pos
]))
496 while(pos
< sel
.size() && is_whitespace(sel
[pos
]))
499 if (pos
== sel
.size()) break;
506 while(pos
< sel
.size())
508 if (sel
[pos
] == '\'' || sel
[pos
] == '"')
510 // skip over quoted value
513 while(pos
< sel
.size() && sel
[pos
] != sel
[start
])
515 if (sel
[pos
] == '\\')
522 if (pos
== sel
.size()) break;
524 else if (sel
[pos
] == '\\')
528 else if (!quote
&& sel
[pos
] == ']')
530 value
= sel
.substr(start
, pos
- start
);
537 if (pos
== sel
.size()) break;
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
);
559 else if (sel
[pos
] == ':')
563 // pseudo element, eg '::before'
564 if (pos
< sel
.size() && sel
[pos
] == ':')
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
] == '('))
579 while(pos
< sel
.size() && open
> 0)
583 else if (sel
[pos
] == '(')
596 std::string key
= toLowerAscii(sel
.substr(start
, pos
- start
));
603 if (key
[0] == ':' || key
== "after" || key
== "before" || key
== "cue" || key
== "first-letter" || key
== "first-line")
605 if (!pseudoElement
.empty())
612 pseudoElement
= ":" + key
;
621 current
.addPseudoClass(key
);
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())
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
]))
646 if (sel
[pos
] == '>' || sel
[pos
] == '+' || sel
[pos
] == '~')
648 current
.Combinator
= sel
[pos
];
651 while(pos
< sel
.size() && is_whitespace(sel
[pos
]))
656 if (sel
[pos
] != ',' && sel
[pos
] != '\0')
657 current
.Combinator
= ' ';
662 current
.Combinator
= sel
[pos
];
678 else if (!current
.empty())
680 result
.push_back(current
);
686 // ***************************************************************************
688 void CCssParser::preprocess()
698 charsLeft
= _Style
.size() - _Position
- 1;
701 if (_Style
[_Position
] == 0x0C || _Style
[_Position
] == 0x0D)
705 if (charsLeft
>= 1 && _Style
[_Position
] == 0x0D && _Style
[_Position
+1] == 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");
720 // strip comments for easier parsing
721 if (_Style
[_Position
] == '\\')
725 else if (is_quote(_Style
[_Position
]))
728 quoteChar
= _Style
[_Position
];
730 if (quote
&& _Style
[_Position
] == quoteChar
)
733 else if (!quote
&& is_comment_open())
735 size_t pos
= _Style
.find("*/", _Position
+ 2);
736 if (pos
== std::string::npos
)
739 _Style
.erase(_Position
, pos
- _Position
+ 2);
741 uc
= _Style
[_Position
];
743 // _Position is already at correct place