cmake: avoid de-duplication of user's CXXFLAGS
[quassel.git] / src / uisupport / qssparser.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2019 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include "qssparser.h"
22
23 #include <tuple>
24 #include <utility>
25
26 #include <QApplication>
27
28 QssParser::QssParser()
29 {
30     _palette = QApplication::palette();
31
32     // Init palette color roles
33     _paletteColorRoles["alternate-base"] = QPalette::AlternateBase;
34     _paletteColorRoles["background"] = QPalette::Window;
35     _paletteColorRoles["base"] = QPalette::Base;
36     _paletteColorRoles["bright-text"] = QPalette::BrightText;
37     _paletteColorRoles["button"] = QPalette::Button;
38     _paletteColorRoles["button-text"] = QPalette::ButtonText;
39     _paletteColorRoles["dark"] = QPalette::Dark;
40     _paletteColorRoles["foreground"] = QPalette::WindowText;
41     _paletteColorRoles["highlight"] = QPalette::Highlight;
42     _paletteColorRoles["highlighted-text"] = QPalette::HighlightedText;
43     _paletteColorRoles["light"] = QPalette::Light;
44     _paletteColorRoles["link"] = QPalette::Link;
45     _paletteColorRoles["link-visited"] = QPalette::LinkVisited;
46     _paletteColorRoles["mid"] = QPalette::Mid;
47     _paletteColorRoles["midlight"] = QPalette::Midlight;
48     _paletteColorRoles["shadow"] = QPalette::Shadow;
49     _paletteColorRoles["text"] = QPalette::Text;
50     _paletteColorRoles["tooltip-base"] = QPalette::ToolTipBase;
51     _paletteColorRoles["tooltip-text"] = QPalette::ToolTipText;
52     _paletteColorRoles["window"] = QPalette::Window;
53     _paletteColorRoles["window-text"] = QPalette::WindowText;
54
55     _uiStylePalette = QVector<QBrush>(static_cast<int>(UiStyle::ColorRole::NumRoles), QBrush());
56
57     _uiStyleColorRoles["marker-line"] = UiStyle::ColorRole::MarkerLine;
58     // Sender colors
59     _uiStyleColorRoles["sender-color-self"] = UiStyle::ColorRole::SenderColorSelf;
60     _uiStyleColorRoles["sender-color-00"] = UiStyle::ColorRole::SenderColor00;
61     _uiStyleColorRoles["sender-color-01"] = UiStyle::ColorRole::SenderColor01;
62     _uiStyleColorRoles["sender-color-02"] = UiStyle::ColorRole::SenderColor02;
63     _uiStyleColorRoles["sender-color-03"] = UiStyle::ColorRole::SenderColor03;
64     _uiStyleColorRoles["sender-color-04"] = UiStyle::ColorRole::SenderColor04;
65     _uiStyleColorRoles["sender-color-05"] = UiStyle::ColorRole::SenderColor05;
66     _uiStyleColorRoles["sender-color-06"] = UiStyle::ColorRole::SenderColor06;
67     _uiStyleColorRoles["sender-color-07"] = UiStyle::ColorRole::SenderColor07;
68     _uiStyleColorRoles["sender-color-08"] = UiStyle::ColorRole::SenderColor08;
69     _uiStyleColorRoles["sender-color-09"] = UiStyle::ColorRole::SenderColor09;
70     _uiStyleColorRoles["sender-color-0a"] = UiStyle::ColorRole::SenderColor0a;
71     _uiStyleColorRoles["sender-color-0b"] = UiStyle::ColorRole::SenderColor0b;
72     _uiStyleColorRoles["sender-color-0c"] = UiStyle::ColorRole::SenderColor0c;
73     _uiStyleColorRoles["sender-color-0d"] = UiStyle::ColorRole::SenderColor0d;
74     _uiStyleColorRoles["sender-color-0e"] = UiStyle::ColorRole::SenderColor0e;
75     _uiStyleColorRoles["sender-color-0f"] = UiStyle::ColorRole::SenderColor0f;
76 }
77
78 void QssParser::processStyleSheet(QString& ss)
79 {
80     if (ss.isEmpty())
81         return;
82
83     // Remove C-style comments /* */ or //
84     static QRegExp commentRx(R"((//.*(\n|$)|/\*.*\*/))");
85     commentRx.setMinimal(true);
86     ss.remove(commentRx);
87
88     // Palette definitions first, so we can apply roles later on
89     static const QRegExp paletterx("(Palette[^{]*)\\{([^}]+)\\}");
90     int pos = 0;
91     while ((pos = paletterx.indexIn(ss, pos)) >= 0) {
92         parsePaletteBlock(paletterx.cap(1).trimmed(), paletterx.cap(2).trimmed());
93         ss.remove(pos, paletterx.matchedLength());
94     }
95
96     // Now we can parse the rest of our custom blocks
97     static const QRegExp blockrx("((?:ChatLine|ChatListItem|NickListItem)[^{]*)\\{([^}]+)\\}");
98     pos = 0;
99     while ((pos = blockrx.indexIn(ss, pos)) >= 0) {
100         // qDebug() << blockrx.cap(1) << blockrx.cap(2);
101         QString declaration = blockrx.cap(1).trimmed();
102         QString contents = blockrx.cap(2).trimmed();
103
104         if (declaration.startsWith("ChatLine"))
105             parseChatLineBlock(declaration, contents);
106         else if (declaration.startsWith("ChatListItem") || declaration.startsWith("NickListItem"))
107             parseListItemBlock(declaration, contents);
108         // else
109         // TODO: add moar here
110
111         ss.remove(pos, blockrx.matchedLength());
112     }
113 }
114
115 /******** Parse a whole block: declaration { contents } *******/
116
117 void QssParser::parseChatLineBlock(const QString& decl, const QString& contents)
118 {
119     UiStyle::FormatType fmtType;
120     UiStyle::MessageLabel label;
121     std::tie(fmtType, label) = parseFormatType(decl);
122     if (fmtType == UiStyle::FormatType::Invalid)
123         return;
124
125     _formats[fmtType | label].merge(parseFormat(contents));
126 }
127
128 void QssParser::parseListItemBlock(const QString& decl, const QString& contents)
129 {
130     UiStyle::ItemFormatType fmtType = parseItemFormatType(decl);
131     if (fmtType == UiStyle::ItemFormatType::Invalid)
132         return;
133
134     _listItemFormats[fmtType].merge(parseFormat(contents));
135 }
136
137 // Palette { ... } specifies the application palette
138 // ColorGroups can be specified like pseudo states, chaining is OR (contrary to normal CSS handling):
139 //   Palette:inactive:disabled { ... } applies to both the Inactive and the Disabled state
140 void QssParser::parsePaletteBlock(const QString& decl, const QString& contents)
141 {
142     QList<QPalette::ColorGroup> colorGroups;
143
144     // Check if we want to apply this palette definition for particular ColorGroups
145     static const QRegExp rx("Palette((:(normal|active|inactive|disabled))*)");
146     if (!rx.exactMatch(decl)) {
147         qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
148         return;
149     }
150     if (!rx.cap(1).isEmpty()) {
151         QStringList groups = rx.cap(1).split(':', QString::SkipEmptyParts);
152         foreach (QString g, groups) {
153             if ((g == "normal" || g == "active") && !colorGroups.contains(QPalette::Active))
154                 colorGroups.append(QPalette::Active);
155             else if (g == "inactive" && !colorGroups.contains(QPalette::Inactive))
156                 colorGroups.append(QPalette::Inactive);
157             else if (g == "disabled" && !colorGroups.contains(QPalette::Disabled))
158                 colorGroups.append(QPalette::Disabled);
159         }
160     }
161
162     // Now let's go through the roles
163     foreach (QString line, contents.split(';', QString::SkipEmptyParts)) {
164         int idx = line.indexOf(':');
165         if (idx <= 0) {
166             qWarning() << Q_FUNC_INFO << tr("Invalid palette role assignment: %1").arg(line.trimmed());
167             continue;
168         }
169         QString rolestr = line.left(idx).trimmed();
170         QString brushstr = line.mid(idx + 1).trimmed();
171
172         if (_paletteColorRoles.contains(rolestr)) {
173             QBrush brush = parseBrush(brushstr);
174             if (colorGroups.count()) {
175                 foreach (QPalette::ColorGroup group, colorGroups)
176                     _palette.setBrush(group, _paletteColorRoles.value(rolestr), brush);
177             }
178             else
179                 _palette.setBrush(_paletteColorRoles.value(rolestr), brush);
180         }
181         else if (_uiStyleColorRoles.contains(rolestr)) {
182             _uiStylePalette[static_cast<int>(_uiStyleColorRoles.value(rolestr))] = parseBrush(brushstr);
183         }
184         else
185             qWarning() << Q_FUNC_INFO << tr("Unknown palette role name: %1").arg(rolestr);
186     }
187 }
188
189 /******** Determine format types from a block declaration ********/
190
191 std::pair<UiStyle::FormatType, UiStyle::MessageLabel> QssParser::parseFormatType(const QString& decl)
192 {
193     using FormatType = UiStyle::FormatType;
194     using MessageLabel = UiStyle::MessageLabel;
195
196     const std::pair<UiStyle::FormatType, UiStyle::MessageLabel> invalid{FormatType::Invalid, MessageLabel::None};
197
198     static const QRegExp rx(R"(ChatLine(?:::(\w+))?(?:#([\w\-]+))?(?:\[([=-,\"\w\s]+)\])?)");
199     // $1: subelement; $2: msgtype; $3: conditionals
200     if (!rx.exactMatch(decl)) {
201         qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
202         return invalid;
203     }
204     QString subElement = rx.cap(1);
205     QString msgType = rx.cap(2);
206     QString conditions = rx.cap(3);
207
208     FormatType fmtType{FormatType::Base};
209     MessageLabel label{MessageLabel::None};
210
211     // First determine the subelement
212     if (!subElement.isEmpty()) {
213         if (subElement == "timestamp")
214             fmtType |= FormatType::Timestamp;
215         else if (subElement == "sender")
216             fmtType |= FormatType::Sender;
217         else if (subElement == "nick")
218             fmtType |= FormatType::Nick;
219         else if (subElement == "contents")
220             fmtType |= FormatType::Contents;
221         else if (subElement == "hostmask")
222             fmtType |= FormatType::Hostmask;
223         else if (subElement == "modeflags")
224             fmtType |= FormatType::ModeFlags;
225         else if (subElement == "url")
226             fmtType |= FormatType::Url;
227         else {
228             qWarning() << Q_FUNC_INFO << tr("Invalid subelement name in %1").arg(decl);
229             return invalid;
230         }
231     }
232
233     // Now, figure out the message type
234     if (!msgType.isEmpty()) {
235         if (msgType == "plain")
236             fmtType |= FormatType::PlainMsg;
237         else if (msgType == "notice")
238             fmtType |= FormatType::NoticeMsg;
239         else if (msgType == "action")
240             fmtType |= FormatType::ActionMsg;
241         else if (msgType == "nick")
242             fmtType |= FormatType::NickMsg;
243         else if (msgType == "mode")
244             fmtType |= FormatType::ModeMsg;
245         else if (msgType == "join")
246             fmtType |= FormatType::JoinMsg;
247         else if (msgType == "part")
248             fmtType |= FormatType::PartMsg;
249         else if (msgType == "quit")
250             fmtType |= FormatType::QuitMsg;
251         else if (msgType == "kick")
252             fmtType |= FormatType::KickMsg;
253         else if (msgType == "kill")
254             fmtType |= FormatType::KillMsg;
255         else if (msgType == "server")
256             fmtType |= FormatType::ServerMsg;
257         else if (msgType == "info")
258             fmtType |= FormatType::InfoMsg;
259         else if (msgType == "error")
260             fmtType |= FormatType::ErrorMsg;
261         else if (msgType == "daychange")
262             fmtType |= FormatType::DayChangeMsg;
263         else if (msgType == "topic")
264             fmtType |= FormatType::TopicMsg;
265         else if (msgType == "netsplit-join")
266             fmtType |= FormatType::NetsplitJoinMsg;
267         else if (msgType == "netsplit-quit")
268             fmtType |= FormatType::NetsplitQuitMsg;
269         else if (msgType == "invite")
270             fmtType |= FormatType::InviteMsg;
271         else {
272             qWarning() << Q_FUNC_INFO << tr("Invalid message type in %1").arg(decl);
273         }
274     }
275
276     // Next up: conditional (formats, labels, nickhash)
277     static const QRegExp condRx(R"lit(\s*([\w\-]+)\s*=\s*"(\w+)"\s*)lit");
278     if (!conditions.isEmpty()) {
279         foreach (const QString& cond, conditions.split(',', QString::SkipEmptyParts)) {
280             if (!condRx.exactMatch(cond)) {
281                 qWarning() << Q_FUNC_INFO << tr("Invalid condition %1").arg(cond);
282                 return invalid;
283             }
284             QString condName = condRx.cap(1);
285             QString condValue = condRx.cap(2);
286             if (condName == "label") {
287                 if (condValue == "highlight")
288                     label |= MessageLabel::Highlight;
289                 else if (condValue == "selected")
290                     label |= MessageLabel::Selected;
291                 else if (condValue == "hovered")
292                     label |= MessageLabel::Hovered;
293                 else {
294                     qWarning() << Q_FUNC_INFO << tr("Invalid message label: %1").arg(condValue);
295                     return invalid;
296                 }
297             }
298             else if (condName == "sender") {
299                 if (condValue == "self")
300                     label |= MessageLabel::OwnMsg;  // sender="self" is actually treated as a label
301                 else {
302                     bool ok = true;
303                     quint32 val = condValue.toUInt(&ok, 16);
304                     if (!ok) {
305                         qWarning() << Q_FUNC_INFO << tr("Invalid senderhash specification: %1").arg(condValue);
306                         return invalid;
307                     }
308                     if (val >= 16) {
309                         qWarning() << Q_FUNC_INFO << tr("Senderhash can be at most \"0x0f\"!");
310                         return invalid;
311                     }
312                     label |= static_cast<MessageLabel>(++val << 16);
313                 }
314             }
315             else if (condName == "format") {
316                 if (condValue == "bold")
317                     fmtType |= FormatType::Bold;
318                 else if (condValue == "italic")
319                     fmtType |= FormatType::Italic;
320                 else if (condValue == "underline")
321                     fmtType |= FormatType::Underline;
322                 else if (condValue == "strikethrough")
323                     fmtType |= FormatType::Strikethrough;
324                 else {
325                     qWarning() << Q_FUNC_INFO << tr("Invalid format name: %1").arg(condValue);
326                     return invalid;
327                 }
328             }
329             else if (condName == "fg-color" || condName == "bg-color") {
330                 bool ok;
331                 quint32 col = condValue.toUInt(&ok, 16);
332                 if (!ok || col > 0x0f) {
333                     qWarning() << Q_FUNC_INFO << tr("Illegal IRC color specification (must be between 00 and 0f): %1").arg(condValue);
334                     return invalid;
335                 }
336                 if (condName == "fg-color")
337                     fmtType |= 0x00400000 | (col << 24);
338                 else
339                     fmtType |= 0x00800000 | (col << 28);
340             }
341             else {
342                 qWarning() << Q_FUNC_INFO << tr("Unhandled condition: %1").arg(condName);
343                 return invalid;
344             }
345         }
346     }
347
348     return std::make_pair(fmtType, label);
349 }
350
351 // FIXME: Code duplication
352 UiStyle::ItemFormatType QssParser::parseItemFormatType(const QString& decl)
353 {
354     using ItemFormatType = UiStyle::ItemFormatType;
355
356     static const QRegExp rx(R"((Chat|Nick)ListItem(?:\[([=-,\"\w\s]+)\])?)");
357     // $1: item type; $2: properties
358     if (!rx.exactMatch(decl)) {
359         qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
360         return ItemFormatType::Invalid;
361     }
362     QString mainItemType = rx.cap(1);
363     QString properties = rx.cap(2);
364
365     ItemFormatType fmtType{ItemFormatType::None};
366
367     // Next up: properties
368     QString type, state;
369     if (!properties.isEmpty()) {
370         QHash<QString, QString> props;
371         static const QRegExp propRx(R"lit(\s*([\w\-]+)\s*=\s*"([\w\-]+)"\s*)lit");
372         foreach (const QString& prop, properties.split(',', QString::SkipEmptyParts)) {
373             if (!propRx.exactMatch(prop)) {
374                 qWarning() << Q_FUNC_INFO << tr("Invalid proplist %1").arg(prop);
375                 return ItemFormatType::Invalid;
376             }
377             props[propRx.cap(1)] = propRx.cap(2);
378         }
379         type = props.value("type");
380         state = props.value("state");
381     }
382
383     if (mainItemType == "Chat") {
384         fmtType |= ItemFormatType::BufferViewItem;
385         if (!type.isEmpty()) {
386             if (type == "network")
387                 fmtType |= ItemFormatType::NetworkItem;
388             else if (type == "channel")
389                 fmtType |= ItemFormatType::ChannelBufferItem;
390             else if (type == "query")
391                 fmtType |= ItemFormatType::QueryBufferItem;
392             else {
393                 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist item type %1").arg(type);
394                 return ItemFormatType::Invalid;
395             }
396         }
397         if (!state.isEmpty()) {
398             if (state == "inactive")
399                 fmtType |= ItemFormatType::InactiveBuffer;
400             else if (state == "channel-event")
401                 fmtType |= ItemFormatType::ActiveBuffer;
402             else if (state == "unread-message")
403                 fmtType |= ItemFormatType::UnreadBuffer;
404             else if (state == "highlighted")
405                 fmtType |= ItemFormatType::HighlightedBuffer;
406             else if (state == "away")
407                 fmtType |= ItemFormatType::UserAway;
408             else {
409                 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist state %1").arg(state);
410                 return ItemFormatType::Invalid;
411             }
412         }
413     }
414     else {  // NickList
415         fmtType |= ItemFormatType::NickViewItem;
416         if (!type.isEmpty()) {
417             if (type == "user") {
418                 fmtType |= ItemFormatType::IrcUserItem;
419                 if (state == "away")
420                     fmtType |= ItemFormatType::UserAway;
421             }
422             else if (type == "category")
423                 fmtType |= ItemFormatType::UserCategoryItem;
424         }
425     }
426     return fmtType;
427 }
428
429 /******** Parse a whole format attribute block ********/
430
431 QTextCharFormat QssParser::parseFormat(const QString& qss)
432 {
433     QTextCharFormat format;
434
435     foreach (QString line, qss.split(';', QString::SkipEmptyParts)) {
436         int idx = line.indexOf(':');
437         if (idx <= 0) {
438             qWarning() << Q_FUNC_INFO << tr("Invalid property declaration: %1").arg(line.trimmed());
439             continue;
440         }
441         QString property = line.left(idx).trimmed();
442         QString value = line.mid(idx + 1).simplified();
443
444         if (property == "background" || property == "background-color")
445             format.setBackground(parseBrush(value));
446         else if (property == "foreground" || property == "color")
447             format.setForeground(parseBrush(value));
448
449         // Color code overrides
450         else if (property == "allow-foreground-override") {
451             bool ok;
452             bool v = parseBoolean(value, &ok);
453             if (ok)
454                 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowForegroundOverride), v);
455         }
456         else if (property == "allow-background-override") {
457             bool ok;
458             bool v = parseBoolean(value, &ok);
459             if (ok)
460                 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowBackgroundOverride), v);
461         }
462
463         // font-related properties
464         else if (property.startsWith("font")) {
465             if (property == "font")
466                 parseFont(value, &format);
467             else if (property == "font-style")
468                 parseFontStyle(value, &format);
469             else if (property == "font-weight")
470                 parseFontWeight(value, &format);
471             else if (property == "font-size")
472                 parseFontSize(value, &format);
473             else if (property == "font-family")
474                 parseFontFamily(value, &format);
475             else {
476                 qWarning() << Q_FUNC_INFO << tr("Invalid font property: %1").arg(line);
477                 continue;
478             }
479         }
480
481         else {
482             qWarning() << Q_FUNC_INFO << tr("Unknown ChatLine property: %1").arg(property);
483         }
484     }
485
486     return format;
487 }
488
489 /******** Boolean value ********/
490
491 bool QssParser::parseBoolean(const QString& str, bool* ok) const
492 {
493     if (ok)
494         *ok = true;
495
496     if (str == "true")
497         return true;
498     if (str == "false")
499         return false;
500
501     qWarning() << Q_FUNC_INFO << tr("Invalid boolean value: %1").arg(str);
502     if (ok)
503         *ok = false;
504     return false;
505 }
506
507 /******** Brush ********/
508
509 QBrush QssParser::parseBrush(const QString& str, bool* ok)
510 {
511     if (ok)
512         *ok = false;
513     QColor c = parseColor(str);
514     if (c.isValid()) {
515         if (ok)
516             *ok = true;
517         return QBrush(c);
518     }
519
520     if (str.startsWith("palette")) {  // Palette color role
521         // Does the palette follow the expected format?  For example:
522         // palette(marker-line)
523         // palette    ( system-color-0f  )
524         //
525         // Match the palette marker, grabbing the name inside in  case-sensitive manner
526         //   palette\s*\(\s*([a-z-0-9]+)\s*\)
527         //   palette   Match the string 'palette'
528         //   \s*       Match any amount of whitespace
529         //   \(, \)    Match literal '(' or ')' marks
530         //   (...+)    Match contents between 1 and unlimited number of times
531         //   [a-z-]    Match any character from a-z, case sensitive
532         //   [0-9]     Match any digit from 0-9
533         // Note that '\' must be escaped as '\\'
534         // Helpful interactive website for debugging and explaining:  https://regex101.com/
535         static const QRegExp rx(R"(palette\s*\(\s*([a-z-0-9]+)\s*\))");
536         if (!rx.exactMatch(str)) {
537             qWarning() << Q_FUNC_INFO << tr("Invalid palette color role specification: %1").arg(str);
538             return QBrush();
539         }
540         if (_paletteColorRoles.contains(rx.cap(1)))
541             return QBrush(_palette.brush(_paletteColorRoles.value(rx.cap(1))));
542         if (_uiStyleColorRoles.contains(rx.cap(1)))
543             return QBrush(_uiStylePalette.at(static_cast<int>(_uiStyleColorRoles.value(rx.cap(1)))));
544         qWarning() << Q_FUNC_INFO << tr("Unknown palette color role: %1").arg(rx.cap(1));
545         return QBrush();
546     }
547     else if (str.startsWith("qlineargradient")) {
548         static const QString rxFloat(R"(\s*(-?\s*[0-9]*\.?[0-9]+)\s*)");
549         static const QRegExp rx(QString(R"(qlineargradient\s*\(\s*x1:%1,\s*y1:%1,\s*x2:%1,\s*y2:%1,(.+)\))").arg(rxFloat));
550         if (!rx.exactMatch(str)) {
551             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
552             return QBrush();
553         }
554         qreal x1 = rx.cap(1).toDouble();
555         qreal y1 = rx.cap(2).toDouble();
556         qreal x2 = rx.cap(3).toDouble();
557         qreal y2 = rx.cap(4).toDouble();
558         QGradientStops stops = parseGradientStops(rx.cap(5).trimmed());
559         if (!stops.count()) {
560             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
561             return QBrush();
562         }
563         QLinearGradient gradient(x1, y1, x2, y2);
564         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
565         gradient.setStops(stops);
566         if (ok)
567             *ok = true;
568         return QBrush(gradient);
569     }
570     else if (str.startsWith("qconicalgradient")) {
571         static const QString rxFloat(R"(\s*(-?\s*[0-9]*\.?[0-9]+)\s*)");
572         static const QRegExp rx(QString(R"(qconicalgradient\s*\(\s*cx:%1,\s*cy:%1,\s*angle:%1,(.+)\))").arg(rxFloat));
573         if (!rx.exactMatch(str)) {
574             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
575             return QBrush();
576         }
577         qreal cx = rx.cap(1).toDouble();
578         qreal cy = rx.cap(2).toDouble();
579         qreal angle = rx.cap(3).toDouble();
580         QGradientStops stops = parseGradientStops(rx.cap(4).trimmed());
581         if (!stops.count()) {
582             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
583             return QBrush();
584         }
585         QConicalGradient gradient(cx, cy, angle);
586         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
587         gradient.setStops(stops);
588         if (ok)
589             *ok = true;
590         return QBrush(gradient);
591     }
592     else if (str.startsWith("qradialgradient")) {
593         static const QString rxFloat(R"(\s*(-?\s*[0-9]*\.?[0-9]+)\s*)");
594         static const QRegExp rx(QString(R"(qradialgradient\s*\(\s*cx:%1,\s*cy:%1,\s*radius:%1,\s*fx:%1,\s*fy:%1,(.+)\))").arg(rxFloat));
595         if (!rx.exactMatch(str)) {
596             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
597             return QBrush();
598         }
599         qreal cx = rx.cap(1).toDouble();
600         qreal cy = rx.cap(2).toDouble();
601         qreal radius = rx.cap(3).toDouble();
602         qreal fx = rx.cap(4).toDouble();
603         qreal fy = rx.cap(5).toDouble();
604         QGradientStops stops = parseGradientStops(rx.cap(6).trimmed());
605         if (!stops.count()) {
606             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
607             return QBrush();
608         }
609         QRadialGradient gradient(cx, cy, radius, fx, fy);
610         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
611         gradient.setStops(stops);
612         if (ok)
613             *ok = true;
614         return QBrush(gradient);
615     }
616
617     return QBrush();
618 }
619
620 QColor QssParser::parseColor(const QString& str)
621 {
622     if (str.startsWith("rgba")) {
623         ColorTuple tuple = parseColorTuple(str.mid(4));
624         if (tuple.count() == 4)
625             return QColor(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));  // NOLINT(modernize-return-braced-init-list)
626     }
627     else if (str.startsWith("rgb")) {
628         ColorTuple tuple = parseColorTuple(str.mid(3));
629         if (tuple.count() == 3)
630             return QColor(tuple.at(0), tuple.at(1), tuple.at(2));
631     }
632     else if (str.startsWith("hsva")) {
633         ColorTuple tuple = parseColorTuple(str.mid(4));
634         if (tuple.count() == 4) {
635             QColor c;
636             c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
637             return c;
638         }
639     }
640     else if (str.startsWith("hsv")) {
641         ColorTuple tuple = parseColorTuple(str.mid(3));
642         if (tuple.count() == 3) {
643             QColor c;
644             c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2));
645             return c;
646         }
647     }
648     else {
649         static const QRegExp rx("#?[0-9A-Fa-z]+");
650         if (rx.exactMatch(str))
651             return QColor(str);
652     }
653     return QColor();
654 }
655
656 // get a list of comma-separated int values or percentages (rel to 0-255)
657 QssParser::ColorTuple QssParser::parseColorTuple(const QString& str)
658 {
659     ColorTuple result;
660     static const QRegExp rx(R"(\(((\s*[0-9]{1,3}%?\s*)(,\s*[0-9]{1,3}%?\s*)*)\))");
661     if (!rx.exactMatch(str.trimmed())) {
662         return ColorTuple();
663     }
664     QStringList values = rx.cap(1).split(',');
665     foreach (QString v, values) {
666         qreal val;
667         bool perc = false;
668         bool ok;
669         v = v.trimmed();
670         if (v.endsWith('%')) {
671             perc = true;
672             v.chop(1);
673         }
674         val = (qreal)v.toUInt(&ok);
675         if (!ok)
676             return ColorTuple();
677         if (perc)
678             val = 255 * val / 100;
679         result.append(val);
680     }
681     return result;
682 }
683
684 QGradientStops QssParser::parseGradientStops(const QString& str_)
685 {
686     QString str = str_;
687     QGradientStops result;
688     static const QString rxFloat("(0?\\.[0-9]+|[01])");  // values between 0 and 1
689     static const QRegExp rx(QString(R"(\s*,?\s*stop:\s*(%1)\s+([^:]+)(,\s*stop:|$))").arg(rxFloat));
690     int idx;
691     while ((idx = rx.indexIn(str)) == 0) {
692         qreal x = rx.cap(1).toDouble();
693         QColor c = parseColor(rx.cap(3));
694         if (!c.isValid())
695             return QGradientStops();
696         result << QGradientStop(x, c);
697         str.remove(0, rx.matchedLength() - rx.cap(4).length());
698     }
699     if (!str.trimmed().isEmpty())
700         return QGradientStops();
701
702     return result;
703 }
704
705 /******** Font Properties ********/
706
707 void QssParser::parseFont(const QString& value, QTextCharFormat* format)
708 {
709     static const QRegExp rx(
710         "((?:(?:normal|italic|oblique|underline|strikethrough|bold|100|200|300|400|500|600|700|800|900) ){0,2}) ?(\\d+)(pt|px)? \"(.*)\"");
711     if (!rx.exactMatch(value)) {
712         qWarning() << Q_FUNC_INFO << tr("Invalid font specification: %1").arg(value);
713         return;
714     }
715     format->setFontItalic(false);
716     format->setFontUnderline(false);
717     format->setFontStrikeOut(false);
718     format->setFontWeight(QFont::Normal);
719     QStringList proplist = rx.cap(1).split(' ', QString::SkipEmptyParts);
720     foreach (QString prop, proplist) {
721         if (prop == "normal")
722             ;  // pass
723         else if (prop == "italic")
724             format->setFontItalic(true);
725         else if (prop == "underline")
726             format->setFontUnderline(true);
727         else if (prop == "strikethrough")
728             format->setFontStrikeOut(true);
729         else if (prop == "oblique")
730             // Oblique is not a property supported by QTextCharFormat
731             format->setFontItalic(true);
732         else if (prop == "bold")
733             format->setFontWeight(QFont::Bold);
734         else {  // number
735             int w = prop.toInt();
736             format->setFontWeight(qMin(w / 8, 99));  // taken from Qt's qss parser
737         }
738     }
739
740     if (rx.cap(3) == "px")
741         format->setProperty(QTextFormat::FontPixelSize, rx.cap(2).toInt());
742     else
743         format->setFontPointSize(rx.cap(2).toInt());
744
745     format->setFontFamily(rx.cap(4));
746 }
747
748 void QssParser::parseFontStyle(const QString& value, QTextCharFormat* format)
749 {
750     if (value == "normal")
751         format->setFontItalic(false);
752     else if (value == "italic")
753         format->setFontItalic(true);
754     else if (value == "underline")
755         format->setFontUnderline(true);
756     else if (value == "strikethrough")
757         format->setFontStrikeOut(true);
758     else if (value == "oblique")
759         // Oblique is not a property supported by QTextCharFormat
760         format->setFontItalic(true);
761     else {
762         qWarning() << Q_FUNC_INFO << tr("Invalid font style specification: %1").arg(value);
763     }
764 }
765
766 void QssParser::parseFontWeight(const QString& value, QTextCharFormat* format)
767 {
768     if (value == "normal")
769         format->setFontWeight(QFont::Normal);
770     else if (value == "bold")
771         format->setFontWeight(QFont::Bold);
772     else {
773         bool ok;
774         int w = value.toInt(&ok);
775         if (!ok) {
776             qWarning() << Q_FUNC_INFO << tr("Invalid font weight specification: %1").arg(value);
777             return;
778         }
779         format->setFontWeight(qMin(w / 8, 99));  // taken from Qt's qss parser
780     }
781 }
782
783 void QssParser::parseFontSize(const QString& value, QTextCharFormat* format)
784 {
785     static const QRegExp rx("(\\d+)(pt|px)");
786     if (!rx.exactMatch(value)) {
787         qWarning() << Q_FUNC_INFO << tr("Invalid font size specification: %1").arg(value);
788         return;
789     }
790     if (rx.cap(2) == "px")
791         format->setProperty(QTextFormat::FontPixelSize, rx.cap(1).toInt());
792     else
793         format->setFontPointSize(rx.cap(1).toInt());
794 }
795
796 void QssParser::parseFontFamily(const QString& value, QTextCharFormat* format)
797 {
798     QString family = value;
799     if (family.startsWith('"') && family.endsWith('"')) {
800         family = family.mid(1, family.length() - 2);
801     }
802     format->setFontFamily(family);
803 }