File indexing completed on 2024-05-26 04:26:22

0001 /* This file is part of the KDE project
0002  * SPDX-FileCopyrightText: 2009 Jan Hambrecht <jaham@gmx.net>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "SvgCssHelper.h"
0008 #include <FlakeDebug.h>
0009 #include <QPair>
0010 
0011 /// Token types used for tokenizing complex selectors
0012 enum CssTokenType {
0013     SelectorToken,  ///< a selector token
0014     CombinatorToken ///< a combinator token
0015 };
0016 
0017 /// A token used for tokenizing complex selectors
0018 typedef QPair<CssTokenType, QString> CssToken;
0019 
0020 /// Selector base class, merely an interface
0021 class CssSelectorBase
0022 {
0023 public:
0024     virtual ~CssSelectorBase() {}
0025     /// Matches the given element
0026     virtual bool match(const QDomElement &) = 0;
0027     /// Returns string representation of selector
0028     virtual QString toString() const { return QString(); }
0029     /**
0030      * Returns priority of selector
0031      * see http://www.w3.org/TR/1998/REC-CSS2-19980512/cascade.html#specificity
0032      */
0033     virtual int priority() { return 0; }
0034 };
0035 
0036 /// Universal selector, matching anything
0037 class UniversalSelector : public CssSelectorBase
0038 {
0039 public:
0040     bool match(const QDomElement &) override
0041     {
0042         // matches always
0043         return true;
0044     }
0045     QString toString() const override
0046     {
0047         return "*";
0048     }
0049 };
0050 
0051 /// Type selector, matching the type of an element
0052 class TypeSelector : public CssSelectorBase
0053 {
0054 public:
0055     TypeSelector(const QString &type)
0056     : m_type(type)
0057     {
0058     }
0059     bool match(const QDomElement &e) override
0060     {
0061         return e.tagName() == m_type;
0062     }
0063     QString toString() const override
0064     {
0065         return m_type;
0066     }
0067     int priority() override
0068     {
0069         return 1;
0070     }
0071 
0072 private:
0073     QString m_type;
0074 };
0075 
0076 /// Id selector, matching the id attribute
0077 class IdSelector : public CssSelectorBase
0078 {
0079 public:
0080     IdSelector(const QString &id)
0081     : m_id(id)
0082     {
0083         if (id.startsWith('#'))
0084             m_id = id.mid(1);
0085     }
0086     bool match(const QDomElement &e) override
0087     {
0088         return e.attribute("id") == m_id;
0089     }
0090     QString toString() const override
0091     {
0092         return '#'+m_id;
0093     }
0094     int priority() override
0095     {
0096         return 100;
0097     }
0098 private:
0099     QString m_id;
0100 };
0101 
0102 /// Attribute selector, matching existence or content of attributes
0103 class AttributeSelector : public CssSelectorBase
0104 {
0105 public:
0106     AttributeSelector(const QString &attribute)
0107     : m_type(Unknown)
0108     {
0109         QString pattern = attribute;
0110         if (pattern.startsWith('['))
0111             pattern.remove(0,1);
0112         if (pattern.endsWith(']'))
0113             pattern.remove(pattern.length()-1,1);
0114         int equalPos = pattern.indexOf('=');
0115         if (equalPos == -1) {
0116             m_type = Exists;
0117             m_attribute = pattern;
0118         } else if (equalPos > 0){
0119             if (pattern[equalPos-1] == '~') {
0120                 m_attribute = pattern.left(equalPos-1);
0121                 m_type = InList;
0122             } else if(pattern[equalPos-1] == '|') {
0123                 m_attribute = pattern.left(equalPos-1) + '-';
0124                 m_type = StartsWith;
0125             } else {
0126                 m_attribute = pattern.left(equalPos);
0127                 m_type = Equals;
0128             }
0129             m_value = pattern.mid(equalPos+1);
0130             if (m_value.startsWith(QLatin1Char('"')))
0131                 m_value.remove(0,1);
0132             if (m_value.endsWith(QLatin1Char('"')))
0133                 m_value.chop(1);
0134         }
0135     }
0136 
0137     bool match(const QDomElement &e) override
0138     {
0139         switch(m_type) {
0140             case Exists:
0141                 return e.hasAttribute(m_attribute);
0142                 break;
0143             case Equals:
0144                 return e.attribute(m_attribute) == m_value;
0145                 break;
0146             case InList:
0147                 {
0148 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
0149                     QStringList tokens = e.attribute(m_attribute).split(' ', Qt::SkipEmptyParts);
0150 #else
0151                     QStringList tokens = e.attribute(m_attribute).split(' ', QString::SkipEmptyParts);
0152 #endif
0153                     return tokens.contains(m_value);
0154                 }
0155                 break;
0156             case StartsWith:
0157                 return e.attribute(m_attribute).startsWith(m_value);
0158                 break;
0159             default:
0160                 return false;
0161         }
0162     }
0163     QString toString() const override
0164     {
0165         QString str('[');
0166         str += m_attribute;
0167         if (m_type == Equals) {
0168             str += '=';
0169         } else if (m_type == InList) {
0170             str += "~=";
0171         } else if (m_type == StartsWith) {
0172             str += "|=";
0173         }
0174         str += m_value;
0175         str += ']';
0176         return str;
0177     }
0178     int priority() override
0179     {
0180         return 10;
0181     }
0182 
0183 private:
0184     enum MatchType {
0185         Unknown,   ///< unknown    -> error state
0186         Exists,    ///< [att]      -> attribute exists
0187         Equals,    ///< [att=val]  -> attribute value matches exactly val
0188         InList,    ///< [att~=val] -> attribute is whitespace separated list where one is val
0189         StartsWith ///< [att|=val] -> attribute starts with val-
0190     };
0191     QString m_attribute;
0192     QString m_value;
0193     MatchType m_type;
0194 };
0195 
0196 /// Pseudo-class selector
0197 class PseudoClassSelector : public CssSelectorBase
0198 {
0199 public:
0200     PseudoClassSelector(const QString &pseudoClass)
0201     : m_pseudoClass(pseudoClass)
0202     {
0203     }
0204 
0205     bool match(const QDomElement &e) override
0206     {
0207         if (m_pseudoClass == ":first-child") {
0208             QDomNode parent = e.parentNode();
0209             if (parent.isNull()) {
0210                 return false;
0211             }
0212             QDomNode firstChild = parent.firstChild();
0213             while(!firstChild.isElement() || firstChild.isNull()) {
0214                 firstChild = firstChild.nextSibling();
0215             }
0216             return firstChild == e;
0217         } else {
0218             return false;
0219         }
0220     }
0221     QString toString() const override
0222     {
0223         return m_pseudoClass;
0224     }
0225     int priority() override
0226     {
0227         return 10;
0228     }
0229 
0230 private:
0231     QString m_pseudoClass;
0232 };
0233 
0234 /// A simple selector, i.e. a type/universal selector followed by attribute, id or pseudo-class selectors
0235 class CssSimpleSelector : public CssSelectorBase
0236 {
0237 public:
0238     CssSimpleSelector(const QString &token)
0239     : m_token(token)
0240     {
0241         compile();
0242     }
0243     ~CssSimpleSelector() override
0244     {
0245         qDeleteAll(m_selectors);
0246     }
0247 
0248     bool match(const QDomElement &e) override
0249     {
0250         Q_FOREACH (CssSelectorBase *s, m_selectors) {
0251             if (!s->match(e))
0252                 return false;
0253         }
0254 
0255         return true;
0256     }
0257 
0258     QString toString() const override
0259     {
0260         QString str;
0261         Q_FOREACH (CssSelectorBase *s, m_selectors) {
0262             str += s->toString();
0263         }
0264         return str;
0265     }
0266     int priority() override
0267     {
0268         int p = 0;
0269         Q_FOREACH (CssSelectorBase *s, m_selectors) {
0270             p += s->priority();
0271         }
0272         return p;
0273     }
0274 
0275 private:
0276     void compile()
0277     {
0278         if (m_token == "*") {
0279             m_selectors.append(new UniversalSelector());
0280             return;
0281         }
0282 
0283         enum {
0284             Start,
0285             Finish,
0286             Bad,
0287             InType,
0288             InId,
0289             InAttribute,
0290             InClassAttribute,
0291             InPseudoClass
0292         } state;
0293 
0294         // add terminator to string
0295         QString expr = m_token + QChar();
0296         int i = 0;
0297         state = Start;
0298 
0299         QString token;
0300         QString sep("#[:.");
0301         // split into base selectors
0302         while((state != Finish) && (state != Bad) && (i < expr.length())) {
0303             QChar ch = expr[i];
0304             switch(state) {
0305                 case Start:
0306                     token += ch;
0307                     if (ch == '#')
0308                         state = InId;
0309                     else if (ch == '[')
0310                         state = InAttribute;
0311                     else if (ch == ':')
0312                         state = InPseudoClass;
0313                     else if (ch == '.')
0314                         state = InClassAttribute;
0315                     else if (ch != '*')
0316                         state = InType;
0317                     break;
0318                 case InAttribute:
0319                     if (ch.isNull()) {
0320                         // reset state and token string
0321                         state = Finish;
0322                         token.clear();
0323                         continue;
0324                     } else {
0325                         token += ch;
0326                         if (ch == ']') {
0327                             m_selectors.append(new AttributeSelector(token));
0328                             state = Start;
0329                             token.clear();
0330                         }
0331                     }
0332                     break;
0333                 case InType:
0334                 case InId:
0335                 case InClassAttribute:
0336                 case InPseudoClass:
0337                     // are we at the start of the next selector or even finished?
0338                     if (sep.contains(ch) || ch.isNull()) {
0339                         if (state == InType)
0340                             m_selectors.append(new TypeSelector(token));
0341                         else if (state == InId)
0342                             m_selectors.append(new IdSelector(token));
0343                         else if ( state == InClassAttribute)
0344                             m_selectors.append(new AttributeSelector("[class~="+token.mid(1)+']'));
0345                         else if (state == InPseudoClass) {
0346                             m_selectors.append(new PseudoClassSelector(token));
0347                         }
0348                         // reset state and token string
0349                         state = ch.isNull() ? Finish : Start;
0350                         token.clear();
0351                         continue;
0352                     } else {
0353                         // append character to current token
0354                         if (!ch.isNull())
0355                             token += ch;
0356                     }
0357                     break;
0358                 default:
0359                     break;
0360             }
0361             i++;
0362         }
0363     }
0364 
0365     QList<CssSelectorBase*> m_selectors;
0366     QString m_token;
0367 };
0368 
0369 /// Complex selector, i.e. a combination of simple selectors
0370 class CssComplexSelector : public CssSelectorBase
0371 {
0372 public:
0373     CssComplexSelector(const QList<CssToken> &tokens)
0374     {
0375         compile(tokens);
0376     }
0377     ~CssComplexSelector() override
0378     {
0379         qDeleteAll(m_selectors);
0380     }
0381     QString toString() const override
0382     {
0383         QString str;
0384         int selectorCount = m_selectors.count();
0385         if (selectorCount) {
0386             for(int i = 0; i < selectorCount-1; ++i) {
0387                 str += m_selectors[i]->toString() +
0388                        m_combinators[i];
0389             }
0390             str += m_selectors.last()->toString();
0391         }
0392         return str;
0393     }
0394 
0395     bool match(const QDomElement &e) override
0396     {
0397         int selectorCount = m_selectors.count();
0398         int combinatorCount = m_combinators.length();
0399         // check count of selectors and combinators
0400         if (selectorCount-combinatorCount != 1)
0401             return false;
0402 
0403         QDomElement currentElement = e;
0404 
0405         // match in reverse order
0406         for(int i = 0; i < selectorCount; ++i) {
0407             CssSelectorBase * curr = m_selectors[selectorCount-1-i];
0408             if (!curr->match(currentElement)) {
0409                 return false;
0410             }
0411             // last selector and still there -> rule matched completely
0412             if(i == selectorCount-1)
0413                 return true;
0414 
0415             CssSelectorBase * next = m_selectors[selectorCount-1-i-1];
0416             QChar combinator = m_combinators[combinatorCount-1-i];
0417             if (combinator == ' ') {
0418                 bool matched = false;
0419                 // descendant combinator
0420                 QDomNode parent = currentElement.parentNode();
0421                 while(!parent.isNull()) {
0422                     currentElement = parent.toElement();
0423                     if (next->match(currentElement)) {
0424                         matched = true;
0425                         break;
0426                     }
0427                     parent = currentElement.parentNode();
0428                 }
0429                 if(!matched)
0430                     return false;
0431             } else if (combinator == '>') {
0432                 // child selector
0433                 QDomNode parent = currentElement.parentNode();
0434                 if (parent.isNull())
0435                     return false;
0436                 QDomElement parentElement = parent.toElement();
0437                 if (next->match(parentElement)) {
0438                     currentElement = parentElement;
0439                 } else {
0440                     return false;
0441                 }
0442             } else if (combinator == '+') {
0443                 QDomNode neighbor = currentElement.previousSibling();
0444                 while(!neighbor.isNull() && !neighbor.isElement())
0445                     neighbor = neighbor.previousSibling();
0446                 if (neighbor.isNull() || !neighbor.isElement())
0447                     return false;
0448                 QDomElement neighborElement = neighbor.toElement();
0449                 if (next->match(neighborElement)) {
0450                     currentElement = neighborElement;
0451                 } else {
0452                     return false;
0453                 }
0454             } else {
0455                 return false;
0456             }
0457         }
0458         return true;
0459     }
0460     int priority() override
0461     {
0462         int p = 0;
0463         Q_FOREACH (CssSelectorBase *s, m_selectors) {
0464             p += s->priority();
0465         }
0466         return p;
0467     }
0468 
0469 private:
0470     void compile(const QList<CssToken> &tokens)
0471     {
0472         Q_FOREACH (const CssToken &token, tokens) {
0473             if(token.first == SelectorToken) {
0474                 m_selectors.append(new CssSimpleSelector(token.second));
0475             } else {
0476                 m_combinators += token.second;
0477             }
0478         }
0479     }
0480 
0481     QString m_combinators;
0482     QList<CssSelectorBase*> m_selectors;
0483 };
0484 
0485 /// A group of selectors (comma separated in css style sheet)
0486 typedef QList<CssSelectorBase*> SelectorGroup;
0487 /// A css rule consisting of group of selectors corresponding to a style
0488 typedef QPair<SelectorGroup, QString> CssRule;
0489 
0490 class SvgCssHelper::Private
0491 {
0492 public:
0493     ~Private()
0494     {
0495         Q_FOREACH (const CssRule &rule, cssRules) {
0496             qDeleteAll(rule.first);
0497         }
0498     }
0499 
0500     SelectorGroup parsePattern(const QString &pattern)
0501     {
0502         SelectorGroup group;
0503 
0504 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
0505         QStringList selectors = pattern.split(',', Qt::SkipEmptyParts);
0506 #else
0507         QStringList selectors = pattern.split(',', QString::SkipEmptyParts);
0508 #endif
0509         for (int i = 0; i < selectors.count(); ++i ) {
0510             CssSelectorBase * selector = compileSelector(selectors[i].simplified());
0511             if (selector)
0512                 group.append(selector);
0513         }
0514         return group;
0515     }
0516 
0517     QList<CssToken> tokenize(const QString &selector)
0518     {
0519         // add terminator to string
0520         QString expr = selector + QChar();
0521         enum {
0522             Finish,
0523             Bad,
0524             InCombinator,
0525             InSelector
0526         } state;
0527 
0528         QChar combinator;
0529         int selectorStart = 0;
0530 
0531         QList<CssToken> tokenList;
0532 
0533         QChar ch = expr[0];
0534         if (ch.isSpace() || ch == '>' || ch == '+') {
0535             debugFlake << "selector starting with combinator is not allowed:" << selector;
0536             return tokenList;
0537         } else {
0538             state = InSelector;
0539             selectorStart = 0;
0540         }
0541         int i = 1;
0542 
0543         // split into simple selectors and combinators
0544         while((state != Finish) && (state != Bad) && (i < expr.length())) {
0545             QChar ch = expr[i];
0546             switch(state) {
0547                 case InCombinator:
0548                     // consume as long as there a combinator characters
0549                     if( ch == '>' || ch == '+') {
0550                         if( ! combinator.isSpace() ) {
0551                             // two non whitespace combinators in sequence are not allowed
0552                             state = Bad;
0553                         } else {
0554                             // switch combinator
0555                             combinator = ch;
0556                         }
0557                     } else if (!ch.isSpace()) {
0558                         tokenList.append(CssToken(CombinatorToken, combinator));
0559                         state = InSelector;
0560                         selectorStart = i;
0561                         combinator = QChar();
0562                     }
0563                     break;
0564                 case InSelector:
0565                     // consume as long as there a non combinator characters
0566                     if (ch.isSpace() || ch == '>' || ch == '+') {
0567                         state = InCombinator;
0568                         combinator = ch;
0569                     } else if (ch.isNull()) {
0570                         state = Finish;
0571                     }
0572                     if (state != InSelector) {
0573                         QString simpleSelector = selector.mid(selectorStart, i-selectorStart);
0574                         tokenList.append(CssToken(SelectorToken, simpleSelector));
0575                     }
0576                     break;
0577                 default:
0578                     break;
0579             }
0580             i++;
0581         }
0582 
0583         return tokenList;
0584     }
0585 
0586     CssSelectorBase * compileSelector(const QString &selector)
0587     {
0588         QList<CssToken> tokenList = tokenize(selector);
0589         if (tokenList.isEmpty())
0590             return 0;
0591 
0592         if (tokenList.count() == 1) {
0593             // simple selector
0594             return new CssSimpleSelector(tokenList.first().second);
0595         } else if (tokenList.count() > 2) {
0596             // complex selector
0597             return new CssComplexSelector(tokenList);
0598         }
0599         return 0;
0600     }
0601 
0602     QMap<QString, QString> cssStyles;
0603     QList<CssRule> cssRules;
0604 };
0605 
0606 SvgCssHelper::SvgCssHelper()
0607 : d(new Private())
0608 {
0609 }
0610 
0611 SvgCssHelper::~SvgCssHelper()
0612 {
0613     delete d;
0614 }
0615 
0616 void SvgCssHelper::parseStylesheet(const QDomElement &e)
0617 {
0618     QString data;
0619 
0620     if (e.hasChildNodes()) {
0621         QDomNode c = e.firstChild();
0622         if (c.isCDATASection()) {
0623             QDomCDATASection cdata = c.toCDATASection();
0624             data = cdata.data().simplified();
0625         } else if (c.isText()) {
0626             QDomText text = c.toText();
0627             data = text.data().simplified();
0628         }
0629     }
0630     if (data.isEmpty())
0631         return;
0632 
0633     // remove comments
0634     QRegExp commentExp("\\/\\*.*\\*\\/");
0635     commentExp.setMinimal(true); // do not match greedy
0636     data.remove(commentExp);
0637 
0638 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
0639     QStringList defs = data.split('}', Qt::SkipEmptyParts);
0640 #else
0641     QStringList defs = data.split('}', QString::SkipEmptyParts);
0642 #endif
0643     for (int i = 0; i < defs.count(); ++i) {
0644         QStringList def = defs[i].split('{');
0645         if( def.count() != 2 )
0646             continue;
0647         QString pattern = def[0].simplified();
0648         if (pattern.isEmpty())
0649             break;
0650         QString style = def[1].simplified();
0651         if (style.isEmpty())
0652             break;
0653 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
0654         QStringList selectors = pattern.split(',', Qt::SkipEmptyParts);
0655 #else
0656         QStringList selectors = pattern.split(',', QString::SkipEmptyParts);
0657 #endif
0658         for (int i = 0; i < selectors.count(); ++i ) {
0659             QString selector = selectors[i].simplified();
0660             d->cssStyles[selector] = style;
0661         }
0662         SelectorGroup group = d->parsePattern(pattern);
0663         d->cssRules.append(CssRule(group, style));
0664     }
0665 }
0666 
0667 QStringList SvgCssHelper::matchStyles(const QDomElement &element) const
0668 {
0669     QMap<int, QString> prioritizedRules;
0670     // match rules to element
0671     Q_FOREACH (const CssRule &rule, d->cssRules) {
0672         Q_FOREACH (CssSelectorBase *s, rule.first) {
0673             bool matched = s->match(element);
0674             if (matched)
0675                 prioritizedRules[s->priority()] = rule.second;
0676         }
0677     }
0678 
0679     // css style attribute has the priority of 100
0680     QString styleAttribute = element.attribute("style").simplified();
0681     if (!styleAttribute.isEmpty())
0682         prioritizedRules[100] = styleAttribute;
0683 
0684     QStringList cssStyles;
0685     // add matching styles in correct order to style list
0686     QMapIterator<int, QString> it(prioritizedRules);
0687     while (it.hasNext()) {
0688         it.next();
0689         cssStyles.append(it.value());
0690     }
0691 
0692     return cssStyles;
0693 }