File indexing completed on 2025-01-05 04:01:18

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #pragma once
0008 
0009 #include "detail.hpp"
0010 
0011 #include <algorithm>
0012 
0013 #include <QDomDocument>
0014 
0015 
0016 namespace glaxnimate::io::svg::detail {
0017 
0018 class CssSelector
0019 {
0020 public:
0021     void set_tag(const QString& tag)
0022     {
0023         if ( tag != '*' && this->tag.isEmpty() )
0024             specificity += 1;
0025         this->tag = tag;
0026     }
0027 
0028     void set_id(const QString& id)
0029     {
0030         if ( this->id.isEmpty() )
0031             specificity += 100;
0032         this->id = id;
0033     }
0034 
0035     void add_class(const QString& class_name)
0036     {
0037         classes.push_back(class_name);
0038         specificity += 10;
0039     }
0040 
0041     bool match(const QDomElement& element, const std::unordered_set<QString>& class_names) const
0042     {
0043         if ( !tag.isEmpty() && tag != "*" && tag != element.tagName() )
0044             return false;
0045 
0046         if ( !id.isEmpty() && id != element.attribute("id") )
0047             return false;
0048 
0049         for ( const auto& class_name : classes )
0050         {
0051             if ( class_names.count(class_name) == 0 )
0052                 return false;
0053         }
0054 
0055         if ( !rule.isEmpty() )
0056             return false;
0057 
0058         return true;
0059     }
0060 
0061     bool empty() const
0062     {
0063         return tag.isEmpty() && id.isEmpty() && classes.empty() && rule.isEmpty();
0064     }
0065 
0066     void set_at_rule(const QString& rule)
0067     {
0068         this->rule = rule;
0069     }
0070 
0071     const QString& at_rule() const
0072     {
0073         return this->rule;
0074     }
0075 
0076 private:
0077     int specificity = 0;
0078     QString tag;
0079     QString id;
0080     QStringList classes;
0081     QString rule;
0082     friend struct CssStyleBlock;
0083 };
0084 
0085 struct CssStyleBlock
0086 {
0087     CssSelector selector;
0088     Style::Map style;
0089 
0090     void merge_into(Style& output) const
0091     {
0092         for ( const auto& p : style )
0093             output[p.first] = p.second;
0094     }
0095 
0096     bool operator<(const CssStyleBlock& other) const
0097     {
0098         return selector.specificity < other.selector.specificity;
0099     }
0100 };
0101 
0102 
0103 class CssParser
0104 {
0105 public:
0106     CssParser(std::vector<CssStyleBlock>& blocks)
0107         : blocks(blocks)
0108     {
0109     }
0110 
0111     void parse(const QString& css)
0112     {
0113         data = css;
0114         index = -1;
0115 
0116         parse_selector();
0117     }
0118 
0119 
0120 private:
0121     enum class TokenType
0122     {
0123         SelectorTag,
0124         SelectorClass,
0125         SelectorId,
0126         SelectorOther,
0127         SelectorComma,
0128         SelectorAt,
0129 
0130         BlockBegin,
0131         BlockEnd,
0132 
0133         RuleName,
0134         RuleColon,
0135         RuleArg,
0136         RuleSemicolon,
0137 
0138         Eof,
0139     };
0140 
0141     using Token = std::pair<TokenType, QString>;
0142 
0143     QChar next_ch_raw()
0144     {
0145         ++index;
0146 
0147         if ( eof() )
0148             return {};
0149 
0150         return data[index];
0151     }
0152 
0153     bool eof() const
0154     {
0155         return index >= data.size();
0156     }
0157 
0158     void back()
0159     {
0160         if ( !eof() )
0161             --index;
0162     }
0163 
0164     QChar next_ch()
0165     {
0166         QChar c = next_ch_raw();
0167 
0168         // Skip comments
0169         if ( c == '/' )
0170         {
0171             QChar d = next_ch_raw();
0172             if ( d == '*' )
0173             {
0174                 while ( true )
0175                 {
0176                     d = next_ch_raw();
0177 
0178                     if ( eof() )
0179                         return {};
0180 
0181                     if ( d == '*' )
0182                     {
0183                         d = next_ch_raw();
0184                         // Treat comments as spaces
0185                         if ( d == '/' )
0186                             return ' ';
0187                         back();
0188                     }
0189                 }
0190             }
0191             else
0192             {
0193                 back();
0194             }
0195         }
0196 
0197         return c;
0198     }
0199 
0200 
0201     static bool is_identifier_start(const QChar& ch)
0202     {
0203         return ch.isLetter() || ch == '_' || ch == '-';
0204     }
0205 
0206     static bool is_identifier(const QChar& ch)
0207     {
0208         return is_identifier_start(ch) || ch.isNumber();
0209     }
0210 
0211     QString lex_identifier()
0212     {
0213         QString id;
0214         QChar ch;
0215 
0216         while ( true )
0217         {
0218             ch = next_ch();
0219             if ( is_identifier(ch) )
0220                 id += ch;
0221             else
0222                 break;
0223         }
0224 
0225         back();
0226 
0227         return id;
0228     }
0229 
0230     QString lex_at_selector()
0231     {
0232         QString id = "@";
0233         QChar ch;
0234 
0235         while ( true )
0236         {
0237             ch = next_ch();
0238             if ( ch == '{' || ch == ',' )
0239                 break;
0240             else
0241                 id += ch;
0242         }
0243 
0244         back();
0245 
0246         return id.trimmed();
0247     }
0248 
0249     Token lex_selector()
0250     {
0251         QChar ch = next_ch();
0252         if ( eof() )
0253             return {TokenType::Eof, {}};
0254 
0255         if ( is_identifier_start(ch) )
0256             return {TokenType::SelectorTag, ch + lex_identifier()};
0257         else if ( ch == '#' )
0258             return {TokenType::SelectorId, lex_identifier()};
0259         else if ( ch == '.' )
0260             return {TokenType::SelectorClass, lex_identifier()};
0261         else if ( ch == ',' )
0262             return {TokenType::SelectorComma, {}};
0263         else if ( ch == '{' )
0264             return {TokenType::BlockBegin, {}};
0265         else if ( ch == '*' )
0266             return {TokenType::SelectorTag, ch};
0267         else if ( ch == '@' )
0268             return {TokenType::SelectorAt, lex_at_selector()};
0269 
0270         if ( ch.isSpace() )
0271         {
0272             skip_space();
0273             ch = next_ch();
0274             if ( ch == ',' )
0275                 return {TokenType::SelectorComma, {}};
0276             if ( ch == '{' )
0277                 return {TokenType::BlockBegin, {}};
0278             back();
0279         }
0280 
0281         return {TokenType::SelectorOther, {}};
0282     }
0283 
0284     Token ignore_selector()
0285     {
0286         Token token = {TokenType::Eof, {}};
0287 
0288         do
0289         {
0290             token = lex_selector();
0291             if ( token.first == TokenType::SelectorComma )
0292                 return lex_selector();
0293         }
0294         while ( token.first != TokenType::Eof && token.first != TokenType::BlockBegin );
0295 
0296         return token;
0297     }
0298 
0299     void skip_space()
0300     {
0301         QChar c;
0302         do
0303         {
0304             c = next_ch();
0305         }
0306         while ( !eof() && c.isSpace() );
0307 
0308         back();
0309     }
0310 
0311     void ignore_block()
0312     {
0313         Token token = {TokenType::Eof, {}};
0314 
0315         do
0316         {
0317             token = lex_selector();
0318         }
0319         while ( token.first != TokenType::Eof && token.first != TokenType::BlockEnd );
0320     }
0321 
0322     bool parse_selector_step(const Token& token)
0323     {
0324         if ( token.first == TokenType::SelectorClass )
0325             selectors.back().add_class(token.second);
0326         else if ( token.first == TokenType::SelectorId )
0327             selectors.back().set_id(token.second);
0328         else if ( token.first == TokenType::SelectorTag )
0329             selectors.back().set_tag(token.second);
0330         else if ( token.first == TokenType::SelectorAt )
0331             selectors.back().set_at_rule(token.second);
0332         else
0333             return false;
0334 
0335         return true;
0336     }
0337 
0338     void parse_selector()
0339     {
0340         while ( true )
0341         {
0342             skip_space();
0343             selectors.clear();
0344             Token token = lex_selector();
0345 
0346             if ( token.first == TokenType::BlockBegin )
0347             {
0348                 ignore_block();
0349                 token = lex_selector();
0350             }
0351 
0352             while ( true )
0353             {
0354                 selectors.push_back({});
0355 
0356                 while ( parse_selector_step(token) )
0357                     token = lex_selector();
0358 
0359                 if ( eof() )
0360                     return;
0361 
0362                 if ( token.first == TokenType::BlockBegin )
0363                 {
0364                     if ( selectors.back().empty() )
0365                         selectors.pop_back();
0366                     break;
0367                 }
0368 
0369                 if ( token.first != TokenType::SelectorComma )
0370                 {
0371                     token = ignore_selector();
0372                     selectors.pop_back();
0373                 }
0374                 else
0375                 {
0376                     skip_space();
0377                     token = lex_selector();
0378                 }
0379             }
0380 
0381             if ( selectors.empty() )
0382                 ignore_block();
0383             else
0384                 parse_block();
0385         }
0386     }
0387 
0388     Token lex_rule()
0389     {
0390         skip_space();
0391 
0392         QChar ch = next_ch();
0393         if ( eof() )
0394             return {TokenType::Eof, {}};
0395 
0396         if ( is_identifier_start(ch) )
0397             return {TokenType::RuleName, ch + lex_identifier()};
0398         else if ( ch == ':' )
0399             return {TokenType::RuleColon, {}};
0400         else if ( ch == ';' )
0401             return {TokenType::RuleSemicolon, {}};
0402         else if ( ch == '}' )
0403             return {TokenType::BlockEnd, {}};
0404 
0405         return {TokenType::RuleArg, ch};
0406 
0407     }
0408 
0409     Token ignore_rule()
0410     {
0411         Token token = lex_rule();
0412         while ( token.first != TokenType::Eof && token.first != TokenType::RuleSemicolon && token.first != TokenType::BlockEnd )
0413             token = lex_rule();
0414         return token;
0415     }
0416 
0417     void lex_quoted_string(QString& value, QChar terminator)
0418     {
0419         while ( true )
0420         {
0421             QChar ch = next_ch();
0422 
0423             if ( eof() )
0424                 break;
0425 
0426             value += ch;
0427 
0428             if ( ch == terminator )
0429                 break;
0430 
0431             if ( ch == '\\' )
0432             {
0433                 ch = next_ch();
0434                 if ( eof() )
0435                     break;
0436                 value += ch;
0437             }
0438         }
0439     }
0440 
0441     Token lex_rule_value(QString& value)
0442     {
0443         if ( value == "\"" || value == "'" )
0444             lex_quoted_string(value, value[0]);
0445 
0446         while ( true )
0447         {
0448             QChar ch = next_ch();
0449             if ( eof() )
0450                 return {TokenType::Eof, {}};
0451             else if ( ch == ';' )
0452                 return {TokenType::RuleSemicolon, {}};
0453             else if ( ch == '}' )
0454                 return {TokenType::BlockEnd, {}};
0455 
0456             value += ch;
0457             if ( ch == '"' || ch == '\'' )
0458             {
0459                 lex_quoted_string(value, ch);
0460             }
0461         }
0462     }
0463 
0464     void parse_block()
0465     {
0466         rules.clear();
0467 
0468         while ( true )
0469         {
0470             Token token = lex_rule();
0471             if ( eof() || token.first == TokenType::BlockEnd )
0472                 break;
0473 
0474             if ( token.first != TokenType::RuleName )
0475             {
0476                 ignore_rule();
0477                 continue;
0478             }
0479 
0480             QString name = token.second;
0481 
0482             if ( lex_rule().first != TokenType::RuleColon )
0483             {
0484                 ignore_rule();
0485                 continue;
0486             }
0487 
0488             token = lex_rule();
0489             if ( eof() || token.first == TokenType::BlockEnd )
0490                 break;
0491             if ( token.first == TokenType::RuleSemicolon )
0492                 continue;
0493 
0494             QString value = token.second;
0495 
0496             token = lex_rule_value(value);
0497             if ( !value.isEmpty() )
0498                 rules[name] = value.trimmed();
0499 
0500             if ( eof() || token.first == TokenType::BlockEnd )
0501                 break;
0502         }
0503 
0504         for ( const auto& selector : selectors )
0505             blocks.push_back({selector, rules});
0506 
0507         rules.clear();
0508         selectors.clear();
0509     }
0510 
0511     QString data;
0512     int index = 0;
0513     std::vector<CssStyleBlock>& blocks;
0514     std::vector<CssSelector> selectors;
0515     Style::Map rules;
0516 };
0517 
0518 } // namespace glaxnimate::io::svg::detail