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/code/nel/src/gui/css_parser.cpp

707 lines
15 KiB
C++

// Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
// Copyright (C) 2010 Winch Gate Property Limited
//
// 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;
std::vector<std::string> elements;
NLMISC::splitString(styleString, ";", elements);
for(uint i = 0; i < elements.size(); ++i)
{
std::string::size_type pos;
pos = elements[i].find_first_of(':');
if (pos != std::string::npos)
{
std::string key = trim(toLower(elements[i].substr(0, pos)));
std::string value = trim(elements[i].substr(pos+1));
styles.push_back(TStylePair(key, value));
}
}
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.fromUtf8(cssString);
preprocess();
_Position = 0;
while(!is_eof())
{
skipWhitespace();
if (_Style[_Position] == (ucchar)'@')
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 ucstring &selectorString, const ucstring &styleString)
{
std::vector<ucstring> selectors;
NLMISC::explode(selectorString, ucstring(","), selectors);
TStyleVec props;
props = parseDecls(styleString.toUtf8());
// duplicate props to each selector in selector list,
// example 'div > p, h1' creates 'div>p' and 'h1'
for(uint i=0; i<selectors.size(); ++i)
{
CCssStyle::SStyleRule rule;
rule.Selector = parse_selector(trim(selectors[i]), rule.PseudoElement);
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] != (ucchar)';')
{
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] == (ucchar)'[')
skipBlock();
else if (_Style[_Position] == (ucchar)'{')
break;
else
_Position++;
}
if (!is_eof())
{
ucstring selector;
selector.append(_Style, start, _Position - start);
skipWhitespace();
// declaration block
start = _Position;
skipBlock();
if (_Position <= _Style.size())
{
ucstring 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] == (ucchar)' ')
_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] == (ucchar)'-')
{
// cannot start with -#
valid = false;
}
}
else if (_Style[_Position] == (ucchar)'_')
{
// valid
}
else if (_Style[_Position] >= 0x0080)
{
// valid
}
else if (_Style[_Position] == (ucchar)'-')
{
if ((_Position - start) == 1 && _Style[_Position-1] == (ucchar)'-')
{
// cannot start with --
valid = false;
}
}
else
{
// we're done
break;
}
_Position++;
}
return valid && !is_eof();
}
// ***************************************************************************
// skip over (..), [..], or {..} blocks
// @internal
void CCssParser::skipBlock()
{
ucchar 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()
{
ucchar 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 ucstring &sel, std::string &pseudoElement) const
{
std::vector<CCssSelector> result;
CCssSelector current;
pseudoElement.clear();
bool failed = false;
ucstring::size_type start = 0, pos = 0;
while(pos < sel.size())
{
ucstring uc;
uc = sel[pos];
if (is_nmchar(sel[pos]) && current.empty())
{
pos++;
while(pos < sel.size() && is_nmchar(sel[pos]))
pos++;
current.Element = toLower(sel.substr(start, pos - start).toUtf8());
start = pos;
continue;
}
if(sel[pos] == '#')
{
pos++;
start=pos;
while(pos < sel.size() && is_nmchar(sel[pos]))
pos++;
current.Id = toLower(sel.substr(start, pos - start).toUtf8());
start = pos;
}
else if (sel[pos] == '.')
{
pos++;
start=pos;
// .classA.classB
while(pos < sel.size() && (is_nmchar(sel[pos]) || sel[pos] == '.'))
pos++;
current.setClass(toLower(sel.substr(start, pos - start).toUtf8()));
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;
}
ucstring key;
ucstring value;
ucchar 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.toUtf8());
}
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] == ' ')
{
ucchar 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.toUtf8(), trimQuotes(value).toUtf8(), (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 = toLower(sel.substr(start, pos - start).toUtf8());
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)
{
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;
ucchar 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++;
ucstring tmp;
tmp += 0x000A;
_Style.replace(_Position, 1, tmp);
}
else if (_Style[_Position] == 0x00)
{
// Unicode replacement character
_Style[_Position] = 0xFFFD;
}
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(ucstring("*/"), _Position + 2);
if (pos == std::string::npos)
pos = _Style.size();
_Style.erase(_Position, pos - _Position + 2);
ucstring uc;
uc = _Style[_Position];
// _Position is already at correct place
continue;
}
}
_Position++;
}
}
} // namespace