Krita Source Code Documentation
Loading...
Searching...
No Matches
SvgCssHelper.cpp
Go to the documentation of this file.
1/* This file is part of the KDE project
2 * SPDX-FileCopyrightText: 2009 Jan Hambrecht <jaham@gmx.net>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7#include "SvgCssHelper.h"
8#include <FlakeDebug.h>
9#include <QPair>
10#include <QRegExp>
11
17
19typedef QPair<CssTokenType, QString> CssToken;
20
23{
24public:
25 virtual ~CssSelectorBase() {}
27 virtual bool match(const QDomElement &) = 0;
29 virtual QString toString() const { return QString(); }
34 virtual int priority() { return 0; }
35};
36
39{
40public:
41 bool match(const QDomElement &) override
42 {
43 // matches always
44 return true;
45 }
46 QString toString() const override
47 {
48 return "*";
49 }
50};
51
54{
55public:
56 TypeSelector(const QString &type)
57 : m_type(type)
58 {
59 }
60 bool match(const QDomElement &e) override
61 {
62 return e.tagName() == m_type;
63 }
64 QString toString() const override
65 {
66 return m_type;
67 }
68 int priority() override
69 {
70 return 1;
71 }
72
73private:
74 QString m_type;
75};
76
79{
80public:
81 IdSelector(const QString &id)
82 : m_id(id)
83 {
84 if (id.startsWith('#'))
85 m_id = id.mid(1);
86 }
87 bool match(const QDomElement &e) override
88 {
89 return e.attribute("id") == m_id;
90 }
91 QString toString() const override
92 {
93 return '#'+m_id;
94 }
95 int priority() override
96 {
97 return 100;
98 }
99private:
100 QString m_id;
101};
102
105{
106public:
107 AttributeSelector(const QString &attribute)
108 : m_type(Unknown)
109 {
110 QString pattern = attribute;
111 if (pattern.startsWith('['))
112 pattern.remove(0,1);
113 if (pattern.endsWith(']'))
114 pattern.remove(pattern.length()-1,1);
115 int equalPos = pattern.indexOf('=');
116 if (equalPos == -1) {
117 m_type = Exists;
118 m_attribute = pattern;
119 } else if (equalPos > 0){
120 if (pattern[equalPos-1] == '~') {
121 m_attribute = pattern.left(equalPos-1);
122 m_type = InList;
123 } else if(pattern[equalPos-1] == '|') {
124 m_attribute = pattern.left(equalPos-1) + '-';
126 } else {
127 m_attribute = pattern.left(equalPos);
128 m_type = Equals;
129 }
130 m_value = pattern.mid(equalPos+1);
131 if (m_value.startsWith(QLatin1Char('"')))
132 m_value.remove(0,1);
133 if (m_value.endsWith(QLatin1Char('"')))
134 m_value.chop(1);
135 }
136 }
137
138 bool match(const QDomElement &e) override
139 {
140 switch(m_type) {
141 case Exists:
142 return e.hasAttribute(m_attribute);
143 break;
144 case Equals:
145 return e.attribute(m_attribute) == m_value;
146 break;
147 case InList:
148 {
149 QStringList tokens = e.attribute(m_attribute).split(' ', Qt::SkipEmptyParts);
150 return tokens.contains(m_value);
151 }
152 break;
153 case StartsWith:
154 return e.attribute(m_attribute).startsWith(m_value);
155 break;
156 default:
157 return false;
158 }
159 }
160 QString toString() const override
161 {
162 QString str('[');
163 str += m_attribute;
164 if (m_type == Equals) {
165 str += '=';
166 } else if (m_type == InList) {
167 str += "~=";
168 } else if (m_type == StartsWith) {
169 str += "|=";
170 }
171 str += m_value;
172 str += ']';
173 return str;
174 }
175 int priority() override
176 {
177 return 10;
178 }
179
180private:
188 QString m_attribute;
189 QString m_value;
191};
192
195{
196public:
197 PseudoClassSelector(const QString &pseudoClass)
198 : m_pseudoClass(pseudoClass)
199 {
200 }
201
202 bool match(const QDomElement &e) override
203 {
204 if (m_pseudoClass == ":first-child") {
205 QDomNode parent = e.parentNode();
206 if (parent.isNull()) {
207 return false;
208 }
209 QDomNode firstChild = parent.firstChild();
210 while(!firstChild.isElement() || firstChild.isNull()) {
211 firstChild = firstChild.nextSibling();
212 }
213 return firstChild == e;
214 } else {
215 return false;
216 }
217 }
218 QString toString() const override
219 {
220 return m_pseudoClass;
221 }
222 int priority() override
223 {
224 return 10;
225 }
226
227private:
229};
230
233{
234public:
235 CssSimpleSelector(const QString &token)
236 : m_token(token)
237 {
238 compile();
239 }
241 {
242 qDeleteAll(m_selectors);
243 }
244
245 bool match(const QDomElement &e) override
246 {
247 Q_FOREACH (CssSelectorBase *s, m_selectors) {
248 if (!s->match(e))
249 return false;
250 }
251
252 return true;
253 }
254
255 QString toString() const override
256 {
257 QString str;
258 Q_FOREACH (CssSelectorBase *s, m_selectors) {
259 str += s->toString();
260 }
261 return str;
262 }
263 int priority() override
264 {
265 int p = 0;
266 Q_FOREACH (CssSelectorBase *s, m_selectors) {
267 p += s->priority();
268 }
269 return p;
270 }
271
272private:
273 void compile()
274 {
275 if (m_token == "*") {
276 m_selectors.append(new UniversalSelector());
277 return;
278 }
279
280 enum {
281 Start,
282 Finish,
283 Bad,
284 InType,
285 InId,
286 InAttribute,
287 InClassAttribute,
288 InPseudoClass
289 } state;
290
291 // add terminator to string
292 QString expr = m_token + QChar();
293 int i = 0;
294 state = Start;
295
296 QString token;
297 QString sep("#[:.");
298 // split into base selectors
299 while((state != Finish) && (state != Bad) && (i < expr.length())) {
300 QChar ch = expr[i];
301 switch(state) {
302 case Start:
303 token += ch;
304 if (ch == '#')
305 state = InId;
306 else if (ch == '[')
307 state = InAttribute;
308 else if (ch == ':')
309 state = InPseudoClass;
310 else if (ch == '.')
311 state = InClassAttribute;
312 else if (ch != '*')
313 state = InType;
314 break;
315 case InAttribute:
316 if (ch.isNull()) {
317 // reset state and token string
318 state = Finish;
319 token.clear();
320 continue;
321 } else {
322 token += ch;
323 if (ch == ']') {
324 m_selectors.append(new AttributeSelector(token));
325 state = Start;
326 token.clear();
327 }
328 }
329 break;
330 case InType:
331 case InId:
332 case InClassAttribute:
333 case InPseudoClass:
334 // are we at the start of the next selector or even finished?
335 if (sep.contains(ch) || ch.isNull()) {
336 if (state == InType)
337 m_selectors.append(new TypeSelector(token));
338 else if (state == InId)
339 m_selectors.append(new IdSelector(token));
340 else if ( state == InClassAttribute)
341 m_selectors.append(new AttributeSelector("[class~="+token.mid(1)+']'));
342 else if (state == InPseudoClass) {
343 m_selectors.append(new PseudoClassSelector(token));
344 }
345 // reset state and token string
346 state = ch.isNull() ? Finish : Start;
347 token.clear();
348 continue;
349 } else {
350 // append character to current token
351 if (!ch.isNull())
352 token += ch;
353 }
354 break;
355 default:
356 break;
357 }
358 i++;
359 }
360 }
361
363 QString m_token;
364};
365
368{
369public:
371 {
372 compile(tokens);
373 }
375 {
376 qDeleteAll(m_selectors);
377 }
378 QString toString() const override
379 {
380 QString str;
381 int selectorCount = m_selectors.count();
382 if (selectorCount) {
383 for(int i = 0; i < selectorCount-1; ++i) {
384 str += m_selectors[i]->toString() +
385 m_combinators[i];
386 }
387 str += m_selectors.last()->toString();
388 }
389 return str;
390 }
391
392 bool match(const QDomElement &e) override
393 {
394 int selectorCount = m_selectors.count();
395 int combinatorCount = m_combinators.length();
396 // check count of selectors and combinators
397 if (selectorCount-combinatorCount != 1)
398 return false;
399
400 QDomElement currentElement = e;
401
402 // match in reverse order
403 for(int i = 0; i < selectorCount; ++i) {
404 CssSelectorBase * curr = m_selectors[selectorCount-1-i];
405 if (!curr->match(currentElement)) {
406 return false;
407 }
408 // last selector and still there -> rule matched completely
409 if(i == selectorCount-1)
410 return true;
411
412 CssSelectorBase * next = m_selectors[selectorCount-1-i-1];
413 QChar combinator = m_combinators[combinatorCount-1-i];
414 if (combinator == ' ') {
415 bool matched = false;
416 // descendant combinator
417 QDomNode parent = currentElement.parentNode();
418 while(!parent.isNull()) {
419 currentElement = parent.toElement();
420 if (next->match(currentElement)) {
421 matched = true;
422 break;
423 }
424 parent = currentElement.parentNode();
425 }
426 if(!matched)
427 return false;
428 } else if (combinator == '>') {
429 // child selector
430 QDomNode parent = currentElement.parentNode();
431 if (parent.isNull())
432 return false;
433 QDomElement parentElement = parent.toElement();
434 if (next->match(parentElement)) {
435 currentElement = parentElement;
436 } else {
437 return false;
438 }
439 } else if (combinator == '+') {
440 QDomNode neighbor = currentElement.previousSibling();
441 while(!neighbor.isNull() && !neighbor.isElement())
442 neighbor = neighbor.previousSibling();
443 if (neighbor.isNull() || !neighbor.isElement())
444 return false;
445 QDomElement neighborElement = neighbor.toElement();
446 if (next->match(neighborElement)) {
447 currentElement = neighborElement;
448 } else {
449 return false;
450 }
451 } else {
452 return false;
453 }
454 }
455 return true;
456 }
457 int priority() override
458 {
459 int p = 0;
460 Q_FOREACH (CssSelectorBase *s, m_selectors) {
461 p += s->priority();
462 }
463 return p;
464 }
465
466private:
467 void compile(const QList<CssToken> &tokens)
468 {
469 Q_FOREACH (const CssToken &token, tokens) {
470 if(token.first == SelectorToken) {
471 m_selectors.append(new CssSimpleSelector(token.second));
472 } else {
473 m_combinators += token.second;
474 }
475 }
476 }
477
480};
481
485typedef QPair<SelectorGroup, QString> CssRule;
486
488{
489public:
491 {
492 Q_FOREACH (const CssRule &rule, cssRules) {
493 qDeleteAll(rule.first);
494 }
495 }
496
497 SelectorGroup parsePattern(const QString &pattern)
498 {
499 SelectorGroup group;
500
501 QStringList selectors = pattern.split(',', Qt::SkipEmptyParts);
502 for (int i = 0; i < selectors.count(); ++i ) {
503 CssSelectorBase * selector = compileSelector(selectors[i].simplified());
504 if (selector)
505 group.append(selector);
506 }
507 return group;
508 }
509
510 QList<CssToken> tokenize(const QString &selector)
511 {
512 // add terminator to string
513 QString expr = selector + QChar();
514 enum {
515 Finish,
516 Bad,
517 InCombinator,
518 InSelector
519 } state;
520
521 QChar combinator;
522 int selectorStart = 0;
523
524 QList<CssToken> tokenList;
525
526 QChar ch = expr[0];
527 if (ch.isSpace() || ch == '>' || ch == '+') {
528 debugFlake << "selector starting with combinator is not allowed:" << selector;
529 return tokenList;
530 } else {
531 state = InSelector;
532 selectorStart = 0;
533 }
534 int i = 1;
535
536 // split into simple selectors and combinators
537 while((state != Finish) && (state != Bad) && (i < expr.length())) {
538 QChar ch = expr[i];
539 switch(state) {
540 case InCombinator:
541 // consume as long as there a combinator characters
542 if( ch == '>' || ch == '+') {
543 if( ! combinator.isSpace() ) {
544 // two non whitespace combinators in sequence are not allowed
545 state = Bad;
546 } else {
547 // switch combinator
548 combinator = ch;
549 }
550 } else if (!ch.isSpace()) {
551 tokenList.append(CssToken(CombinatorToken, combinator));
552 state = InSelector;
553 selectorStart = i;
554 combinator = QChar();
555 }
556 break;
557 case InSelector:
558 // consume as long as there a non combinator characters
559 if (ch.isSpace() || ch == '>' || ch == '+') {
560 state = InCombinator;
561 combinator = ch;
562 } else if (ch.isNull()) {
563 state = Finish;
564 }
565 if (state != InSelector) {
566 QString simpleSelector = selector.mid(selectorStart, i-selectorStart);
567 tokenList.append(CssToken(SelectorToken, simpleSelector));
568 }
569 break;
570 default:
571 break;
572 }
573 i++;
574 }
575
576 return tokenList;
577 }
578
579 CssSelectorBase * compileSelector(const QString &selector)
580 {
581 QList<CssToken> tokenList = tokenize(selector);
582 if (tokenList.isEmpty())
583 return 0;
584
585 if (tokenList.count() == 1) {
586 // simple selector
587 return new CssSimpleSelector(tokenList.first().second);
588 } else if (tokenList.count() > 2) {
589 // complex selector
590 return new CssComplexSelector(tokenList);
591 }
592 return 0;
593 }
594
595 QMap<QString, QString> cssStyles;
597};
598
600: d(new Private())
601{
602}
603
605{
606 delete d;
607}
608
609void SvgCssHelper::parseStylesheet(const QDomElement &e)
610{
611 QString data;
612
613 if (e.hasChildNodes()) {
614 QDomNode c = e.firstChild();
615 if (c.isCDATASection()) {
616 QDomCDATASection cdata = c.toCDATASection();
617 data = cdata.data().simplified();
618 } else if (c.isText()) {
619 QDomText text = c.toText();
620 data = text.data().simplified();
621 }
622 }
623 if (data.isEmpty())
624 return;
625
626 // remove comments
627 QRegExp commentExp("\\/\\*.*\\*\\/");
628 commentExp.setMinimal(true); // do not match greedy
629#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
630 data.remove(commentExp);
631#else
632 commentExp.removeIn(data);
633#endif
634
635 QStringList defs = data.split('}', Qt::SkipEmptyParts);
636 for (int i = 0; i < defs.count(); ++i) {
637 QStringList def = defs[i].split('{');
638 if( def.count() != 2 )
639 continue;
640 QString pattern = def[0].simplified();
641 if (pattern.isEmpty())
642 break;
643 QString style = def[1].simplified();
644 if (style.isEmpty())
645 break;
646 QStringList selectors = pattern.split(',', Qt::SkipEmptyParts);
647 for (int i = 0; i < selectors.count(); ++i ) {
648 QString selector = selectors[i].simplified();
649 d->cssStyles[selector] = style;
650 }
651 SelectorGroup group = d->parsePattern(pattern);
652 d->cssRules.append(CssRule(group, style));
653 }
654}
655
656QStringList SvgCssHelper::matchStyles(const QDomElement &element) const
657{
658 QMap<int, QString> prioritizedRules;
659 // match rules to element
660 Q_FOREACH (const CssRule &rule, d->cssRules) {
661 Q_FOREACH (CssSelectorBase *s, rule.first) {
662 bool matched = s->match(element);
663 if (matched)
664 prioritizedRules[s->priority()] = rule.second;
665 }
666 }
667
668 // css style attribute has the priority of 100
669 QString styleAttribute = element.attribute("style").simplified();
670 if (!styleAttribute.isEmpty())
671 prioritizedRules[100] = styleAttribute;
672
673 QStringList cssStyles;
674 // add matching styles in correct order to style list
675 QMapIterator<int, QString> it(prioritizedRules);
676 while (it.hasNext()) {
677 it.next();
678 cssStyles.append(it.value());
679 }
680
681 return cssStyles;
682}
#define debugFlake
Definition FlakeDebug.h:15
const Params2D p
CssTokenType
Token types used for tokenizing complex selectors.
@ SelectorToken
a selector token
@ CombinatorToken
a combinator token
QPair< CssTokenType, QString > CssToken
A token used for tokenizing complex selectors.
QList< CssSelectorBase * > SelectorGroup
A group of selectors (comma separated in css style sheet)
QPair< SelectorGroup, QString > CssRule
A css rule consisting of group of selectors corresponding to a style.
Attribute selector, matching existence or content of attributes.
int priority() override
AttributeSelector(const QString &attribute)
bool match(const QDomElement &e) override
Matches the given element.
QString toString() const override
Returns string representation of selector.
@ Unknown
unknown -> error state
@ StartsWith
[att|=val] -> attribute starts with val-
@ Exists
[att] -> attribute exists
@ InList
[att~=val] -> attribute is whitespace separated list where one is val
@ Equals
[att=val] -> attribute value matches exactly val
Complex selector, i.e. a combination of simple selectors.
QList< CssSelectorBase * > m_selectors
bool match(const QDomElement &e) override
Matches the given element.
QString toString() const override
Returns string representation of selector.
void compile(const QList< CssToken > &tokens)
int priority() override
CssComplexSelector(const QList< CssToken > &tokens)
~CssComplexSelector() override
Selector base class, merely an interface.
virtual bool match(const QDomElement &)=0
Matches the given element.
virtual ~CssSelectorBase()
virtual int priority()
virtual QString toString() const
Returns string representation of selector.
A simple selector, i.e. a type/universal selector followed by attribute, id or pseudo-class selectors...
bool match(const QDomElement &e) override
Matches the given element.
int priority() override
CssSimpleSelector(const QString &token)
~CssSimpleSelector() override
QString toString() const override
Returns string representation of selector.
QList< CssSelectorBase * > m_selectors
Id selector, matching the id attribute.
QString toString() const override
Returns string representation of selector.
IdSelector(const QString &id)
bool match(const QDomElement &e) override
Matches the given element.
int priority() override
Pseudo-class selector.
int priority() override
QString toString() const override
Returns string representation of selector.
bool match(const QDomElement &e) override
Matches the given element.
PseudoClassSelector(const QString &pseudoClass)
QList< CssToken > tokenize(const QString &selector)
QList< CssRule > cssRules
QMap< QString, QString > cssStyles
SelectorGroup parsePattern(const QString &pattern)
CssSelectorBase * compileSelector(const QString &selector)
Private *const d
QStringList matchStyles(const QDomElement &element) const
void parseStylesheet(const QDomElement &)
Parses css style sheet in given xml element.
Type selector, matching the type of an element.
int priority() override
QString toString() const override
Returns string representation of selector.
bool match(const QDomElement &e) override
Matches the given element.
TypeSelector(const QString &type)
Universal selector, matching anything.
QString toString() const override
Returns string representation of selector.
bool match(const QDomElement &) override
Matches the given element.