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