You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ryzom-core/nel/src/gui/css_parser.cpp

753 lines
16 KiB
C++

// Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
// Copyright (C) 2010-2021 Winch Gate Property Limited
//
// This source file has been modified by the following contributors:
// Copyright (C) 2020 Jan BOON (Kaetemi) <jan.boon@kaetemi.be>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "stdpch.h"
#include <string>
#include "nel/misc/types_nl.h"
#include "nel/gui/css_parser.h"
#include "nel/gui/css_style.h"
#include "nel/gui/css_selector.h"
using namespace NLMISC;
#ifdef DEBUG_NEW
#define new DEBUG_NEW
#endif
namespace NLGUI
{
// ***************************************************************************
// Parse style declarations style, eg. "color:red; font-size: 10px;"
//
// key is converted to lowercase
// value is left as is
TStyleVec CCssParser::parseDecls(const std::string &styleString)
{
TStyleVec styles;
size_t pos = 0;
size_t end = styleString.size();
while(pos < end)
{
size_t sep = styleString.find(':', pos);
if (sep == std::string::npos)
break;
size_t keyIndex = pos;
size_t keyLength = sep - pos;
sep++;
pos = sep;
while(sep < end)
{
sep = styleString.find_first_of(";'\"(", sep);
if (sep == std::string::npos || styleString[sep] == ';')
break;
if (styleString[sep] == '\'' || styleString[sep] == '"')
{
char ch = styleString[sep];
// skip open quote
sep++;
while(sep < end && styleString[sep] != ch)
{
if (styleString[sep] == '\\')
sep++;
sep++;
}
// skip close quote
sep++;
}
else if (styleString[sep] == '(')
{
while(sep < end && styleString[sep] != ')')
{
sep++;
}
// skip close parenthesis
sep++;
}
}
styles.push_back(TStylePair(trim(styleString.substr(keyIndex, keyLength)), trim(styleString.substr(pos, sep - pos))));
if (sep >= end)
break;
pos = sep + 1;
}
return styles;
}
// ***************************************************************************
// Parse stylesheet, eg content from main.css file
//
// Return all found rules
void CCssParser::parseStylesheet(const std::string &cssString, std::vector<CCssStyle::SStyleRule> &result)
{
_Rules.clear();
_Style.clear();
_Style = cssString;
preprocess();
_Position = 0;
while(!is_eof())
{
skipWhitespace();
if (_Style[_Position] == '@')
readAtRule();
else
readRule();
}
result.insert(result.end(), _Rules.begin(), _Rules.end());
_Rules.clear();
}
// ***************************************************************************
// Parse selector with style string
// selector: "a#id .class"
// style: "color: red; font-size: 10px;"
//
// @internal
void CCssParser::parseRule(const std::string &selectorString, const std::string &styleString)
{
TStyleVec props;
props = parseDecls(styleString);
// duplicate props to each selector in selector list,
// example 'div > p, h1' creates 'div>p' and 'h1'
for(std::string::size_type pos = 0; pos < selectorString.size(); pos++)
{
while(pos < selectorString.size() && is_whitespace(selectorString[pos]))
pos++;
CCssStyle::SStyleRule rule;
rule.Selector = parse_selector(selectorString, rule.PseudoElement, pos);
rule.Properties = props;
if (!rule.Selector.empty())
_Rules.push_back(rule);
}
}
// ***************************************************************************
// Skip over at-rule
// @import ... ;
// @charset ... ;
// @media query { .. }
//
// @internal
void CCssParser::readAtRule()
{
// skip '@'
_Position++;
// skip 'import', 'media', etc
skipIdentifier();
// skip at-rule statement
while(!is_eof() && _Style[_Position] != ';')
{
if (maybe_escape())
{
escape();
}
else if (is_quote(_Style[_Position]))
{
skipString();
}
else if (is_block_open(_Style[_Position]))
{
bool mustBreak = (_Style[_Position] == '{');
skipBlock();
if(mustBreak)
{
break;
}
}
else
{
_Position++;
}
}
// skip ';' or '}'
_Position++;
}
// ***************************************************************************
// skip over "elm#id.selector[attr]:peseudo, .sel2 { rule }" block
// @internal
void CCssParser::readRule()
{
size_t start;
// selector
start = _Position;
while(!is_eof())
{
if (maybe_escape())
_Position++;
else if (is_quote(_Style[_Position]))
skipString();
else if (_Style[_Position] == '[')
skipBlock();
else if (_Style[_Position] == '{')
break;
else
_Position++;
}
if (!is_eof())
{
std::string selector;
selector.append(_Style, start, _Position - start);
skipWhitespace();
// declaration block
start = _Position;
skipBlock();
if (_Position <= _Style.size())
{
std::string rules;
rules.append(_Style, start + 1, _Position - start - 2);
parseRule(selector, rules);
}
}
}
// ***************************************************************************
// skip over \abcdef escaped sequence or escaped newline char
// @internal
void CCssParser::escape()
{
// skip '\'
_Position++;
if (is_hex(_Style[_Position]))
{
// TODO: '\abc def' should be considered one string
for(uint i=0; i<6 && is_hex(_Style[_Position]); i++)
_Position++;
if (_Style[_Position] == ' ')
_Position++;
}
else if (_Style[_Position] != 0x0A)
_Position++;
}
// ***************************************************************************
// @internal
bool CCssParser::skipIdentifier()
{
size_t start = _Position;
bool valid = true;
while(!is_eof() && valid)
{
if (maybe_escape())
{
escape();
continue;
}
else if (is_alpha(_Style[_Position]))
{
// valid
}
else if (is_digit(_Style[_Position]))
{
if (_Position == start)
{
// cannot start with digit
valid = false;
}
else if ((_Position - start) == 0 && _Style[_Position-1] == '-')
{
// cannot start with -#
valid = false;
}
}
else if (_Style[_Position] == '_')
{
// valid
}
else if (_Style[_Position] >= 0x80)
{
// valid
}
else if (_Style[_Position] == '-')
{
if ((_Position - start) == 1 && _Style[_Position-1] == '-')
{
// cannot start with --
valid = false;
}
}
else
{
// we're done
break;
}
_Position++;
}
return valid && !is_eof();
}
// ***************************************************************************
// skip over (..), [..], or {..} blocks
// @internal
void CCssParser::skipBlock()
{
char startChar = _Style[_Position];
// block start
_Position++;
while(!is_eof() && !is_block_close(_Style[_Position], startChar))
{
if (maybe_escape())
// skip backslash and next char
_Position += 2;
else if (is_quote(_Style[_Position]))
skipString();
else if (is_block_open(_Style[_Position]))
skipBlock();
else
_Position++;
}
// block end
_Position++;
}
// ***************************************************************************
// skip over quoted string
// @internal
void CCssParser::skipString()
{
char endChar = _Style[_Position];
// quote start
_Position++;
while(!is_eof() && _Style[_Position] != endChar)
{
if (maybe_escape())
_Position++;
_Position++;
}
// quote end
_Position++;
}
// ***************************************************************************
// @internal
void CCssParser::skipWhitespace()
{
while(!is_eof() && is_whitespace(_Style[_Position]))
_Position++;
}
// ***************************************************************************
// parse selector list
// @internal
std::vector<CCssSelector> CCssParser::parse_selector(const std::string &sel, std::string &pseudoElement, std::string::size_type &pos) const
{
std::vector<CCssSelector> result;
CCssSelector current;
pseudoElement.clear();
bool failed = false;
std::string::size_type start = pos;
while(pos < sel.size() && sel[pos] != ',')
{
std::string uc;
uc = sel[pos];
if (is_nmchar(sel[pos]) && current.empty())
{
pos++;
while(pos < sel.size() && is_nmchar(sel[pos]))
pos++;
current.Element = toLowerAscii(sel.substr(start, pos - start));
start = pos;
continue;
}
if (sel[pos] == '*' && current.empty())
{
pos++;
current.Element = "*";
start = pos;
continue;
}
if(sel[pos] == '#')
{
pos++;
start=pos;
while(pos < sel.size() && is_nmchar(sel[pos]))
pos++;
current.Id = toLowerAscii(sel.substr(start, pos - start));
start = pos;
}
else if (sel[pos] == '.')
{
pos++;
start=pos;
// .classA.classB
while(pos < sel.size() && (is_nmchar(sel[pos]) || sel[pos] == '.'))
pos++;
current.setClass(toLowerAscii(sel.substr(start, pos - start)));
start = pos;
}
else if (sel[pos] == '[')
{
pos++;
start = pos;
if (is_whitespace(sel[pos]))
{
while(pos < sel.size() && is_whitespace(sel[pos]))
pos++;
start = pos;
}
std::string key;
std::string value;
char op = ' ';
// key
while(pos < sel.size() && is_nmchar(sel[pos]))
pos++;
key = sel.substr(start, pos - start);
if (pos == sel.size()) break;
if (is_whitespace(sel[pos]))
{
while(pos < sel.size() && is_whitespace(sel[pos]))
pos++;
if (pos == sel.size()) break;
}
if (sel[pos] == ']')
{
current.addAttribute(key);
}
else
{
// operand
op = sel[pos];
if (op == '~' || op == '|' || op == '^' || op == '$' || op == '*')
{
pos++;
if (pos == sel.size()) break;
}
// invalid rule?, eg [attr^value]
if (sel[pos] != '=')
{
while(pos < sel.size() && sel[pos] != ']')
pos++;
if (pos == sel.size()) break;
start = pos;
}
else
{
// skip '='
pos++;
if (is_whitespace(sel[pos]))
{
while(pos < sel.size() && is_whitespace(sel[pos]))
pos++;
if (pos == sel.size()) break;
}
// value
start = pos;
bool quote = false;
char quoteOpen;
while(pos < sel.size())
{
if (sel[pos] == '\'' || sel[pos] == '"')
{
// skip over quoted value
start = pos;
pos++;
while(pos < sel.size() && sel[pos] != sel[start])
{
if (sel[pos] == '\\')
{
pos++;
}
pos++;
}
if (pos == sel.size()) break;
}
else if (sel[pos] == '\\')
{
pos++;
}
else if (!quote && sel[pos] == ']')
{
value = sel.substr(start, pos - start);
break;
}
pos++;
} // while 'value'
if (pos == sel.size()) break;
bool cs = true;
// [value="attr" i]
if (value.size() > 2 && value[value.size()-2] == ' ')
{
char lastChar = value[value.size()-1];
if (lastChar == 'i' || lastChar == 'I' || lastChar == 's' || lastChar == 'S')
{
value = value.substr(0, value.size()-2);
cs = !((lastChar == 'i' || lastChar == 'I'));
}
}
current.addAttribute(key, trimQuotes(value), (char)op, cs);
} // op error
} // no value
// skip ']'
pos++;
start = pos;
}
else if (sel[pos] == ':')
{
pos++;
start=pos;
// pseudo element, eg '::before'
if (pos < sel.size() && sel[pos] == ':')
{
pos++;
}
// :first-child
// :nth-child(2n+0)
// :not(h1, div#main)
// :not(:nth-child(2n+0))
// has no support for quotes, eg :not(h1[attr=")"]) fails
while(pos < sel.size() && (is_nmchar(sel[pos]) || sel[pos] == '('))
{
if (sel[pos] == '(')
{
uint open = 1;
pos++;
while(pos < sel.size() && open > 0)
{
if (sel[pos] == ')')
open--;
else if (sel[pos] == '(')
open++;
pos++;
}
break;
}
else
{
pos++;
}
}
std::string key = toLowerAscii(sel.substr(start, pos - start));
if (key.empty())
{
failed = true;
break;
}
if (key[0] == ':' || key == "after" || key == "before" || key == "cue" || key == "first-letter" || key == "first-line")
{
if (!pseudoElement.empty())
{
failed = true;
break;
}
if (key[0] != ':')
{
pseudoElement = ":" + key;
}
else
{
pseudoElement = key;
}
}
else
{
current.addPseudoClass(key);
}
start = pos;
}
else if (!current.empty())
{
// pseudo element like ':before' can only be set on the last selector
// user action pseudo classes can be used after pseudo element (ie, :focus, :hover)
// there is no support for those and its safe to just fail the selector
if (!result.empty() && !pseudoElement.empty())
{
failed = true;
break;
}
// start new selector as combinator is part of next selector
result.push_back(current);
current = CCssSelector();
// detect and remove whitespace around combinator, eg ' > '
bool isSpace = is_whitespace(sel[pos]);;
while(pos < sel.size() && is_whitespace(sel[pos]))
pos++;
if (sel[pos] == '>' || sel[pos] == '+' || sel[pos] == '~')
{
current.Combinator = sel[pos];
pos++;
while(pos < sel.size() && is_whitespace(sel[pos]))
pos++;
}
else if (isSpace)
{
if (sel[pos] != ',' && sel[pos] != '\0')
current.Combinator = ' ';
}
else
{
// unknown
current.Combinator = sel[pos];
pos++;
}
start = pos;
}
else
{
pos++;
}
}
if (failed)
{
result.clear();
}
else if (!current.empty())
{
result.push_back(current);
}
return result;
}
// ***************************************************************************
// @internal
void CCssParser::preprocess()
{
_Position = 0;
size_t start;
size_t charsLeft;
bool quote = false;
char quoteChar;
while(!is_eof())
{
charsLeft = _Style.size() - _Position - 1;
// FF, CR
if (_Style[_Position] == 0x0C || _Style[_Position] == 0x0D)
{
uint len = 1;
// CR, LF
if (charsLeft >= 1 && _Style[_Position] == 0x0D && _Style[_Position+1] == 0x0A)
len++;
std::string tmp;
tmp += 0x0A;
_Style.replace(_Position, 1, tmp);
}
else if (_Style[_Position] == 0x00)
{
// Unicode replacement character
// _Style[_Position] = 0xFFFD;
_Style.replace(_Position, 1, "\xEF\xBF\xBD");
}
else
{
// strip comments for easier parsing
if (_Style[_Position] == '\\')
{
_Position++;
}
else if (is_quote(_Style[_Position]))
{
if (!quote)
quoteChar = _Style[_Position];
if (quote && _Style[_Position] == quoteChar)
quote = !quote;
}
else if (!quote && is_comment_open())
{
size_t pos = _Style.find("*/", _Position + 2);
if (pos == std::string::npos)
pos = _Style.size();
_Style.erase(_Position, pos - _Position + 2);
std::string uc;
uc = _Style[_Position];
// _Position is already at correct place
continue;
}
}
_Position++;
}
}
} // namespace