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 }