166648f174239c452f027d79c57d7abaecbee1c8
[quassel.git] / src / uisupport / qssparser.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 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 == "strikethrough")
326                     fmtType |= FormatType::Strikethrough;
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         // Color code overrides
455         else if (property == "allow-foreground-override") {
456             bool ok;
457             bool v = parseBoolean(value, &ok);
458             if (ok)
459                 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowForegroundOverride), v);
460         }
461         else if (property == "allow-background-override") {
462             bool ok;
463             bool v = parseBoolean(value, &ok);
464             if (ok)
465                 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowBackgroundOverride), v);
466         }
467
468         // font-related properties
469         else if (property.startsWith("font")) {
470             if (property == "font")
471                 parseFont(value, &format);
472             else if (property == "font-style")
473                 parseFontStyle(value, &format);
474             else if (property == "font-weight")
475                 parseFontWeight(value, &format);
476             else if (property == "font-size")
477                 parseFontSize(value, &format);
478             else if (property == "font-family")
479                 parseFontFamily(value, &format);
480             else {
481                 qWarning() << Q_FUNC_INFO << tr("Invalid font property: %1").arg(line);
482                 continue;
483             }
484         }
485
486         else {
487             qWarning() << Q_FUNC_INFO << tr("Unknown ChatLine property: %1").arg(property);
488         }
489     }
490
491     return format;
492 }
493
494 /******** Boolean value ********/
495
496 bool QssParser::parseBoolean(const QString &str, bool *ok) const
497 {
498     if (ok)
499         *ok = true;
500
501     if (str == "true")
502         return true;
503     if (str == "false")
504         return false;
505
506     qWarning() << Q_FUNC_INFO << tr("Invalid boolean value: %1").arg(str);
507     if (ok)
508         *ok = false;
509     return false;
510 }
511
512 /******** Brush ********/
513
514 QBrush QssParser::parseBrush(const QString &str, bool *ok)
515 {
516     if (ok)
517         *ok = false;
518     QColor c = parseColor(str);
519     if (c.isValid()) {
520         if (ok)
521             *ok = true;
522         return QBrush(c);
523     }
524
525     if (str.startsWith("palette")) { // Palette color role
526         // Does the palette follow the expected format?  For example:
527         // palette(marker-line)
528         // palette    ( system-color-0f  )
529         //
530         // Match the palette marker, grabbing the name inside in  case-sensitive manner
531         //   palette\s*\(\s*([a-z-0-9]+)\s*\)
532         //   palette   Match the string 'palette'
533         //   \s*       Match any amount of whitespace
534         //   \(, \)    Match literal '(' or ')' marks
535         //   (...+)    Match contents between 1 and unlimited number of times
536         //   [a-z-]    Match any character from a-z, case sensitive
537         //   [0-9]     Match any digit from 0-9
538         // Note that '\' must be escaped as '\\'
539         // Helpful interactive website for debugging and explaining:  https://regex101.com/
540         static const QRegExp rx("palette\\s*\\(\\s*([a-z-0-9]+)\\s*\\)");
541         if (!rx.exactMatch(str)) {
542             qWarning() << Q_FUNC_INFO << tr("Invalid palette color role specification: %1").arg(str);
543             return QBrush();
544         }
545         if (_paletteColorRoles.contains(rx.cap(1)))
546             return QBrush(_palette.brush(_paletteColorRoles.value(rx.cap(1))));
547         if (_uiStyleColorRoles.contains(rx.cap(1)))
548             return QBrush(_uiStylePalette.at(static_cast<int>(_uiStyleColorRoles.value(rx.cap(1)))));
549         qWarning() << Q_FUNC_INFO << tr("Unknown palette color role: %1").arg(rx.cap(1));
550         return QBrush();
551     }
552     else if (str.startsWith("qlineargradient")) {
553         static const QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
554         static const QRegExp rx(QString("qlineargradient\\s*\\(\\s*x1:%1,\\s*y1:%1,\\s*x2:%1,\\s*y2:%1,(.+)\\)").arg(rxFloat));
555         if (!rx.exactMatch(str)) {
556             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
557             return QBrush();
558         }
559         qreal x1 = rx.cap(1).toDouble();
560         qreal y1 = rx.cap(2).toDouble();
561         qreal x2 = rx.cap(3).toDouble();
562         qreal y2 = rx.cap(4).toDouble();
563         QGradientStops stops = parseGradientStops(rx.cap(5).trimmed());
564         if (!stops.count()) {
565             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
566             return QBrush();
567         }
568         QLinearGradient gradient(x1, y1, x2, y2);
569         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
570         gradient.setStops(stops);
571         if (ok)
572             *ok = true;
573         return QBrush(gradient);
574     }
575     else if (str.startsWith("qconicalgradient")) {
576         static const QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
577         static const QRegExp rx(QString("qconicalgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*angle:%1,(.+)\\)").arg(rxFloat));
578         if (!rx.exactMatch(str)) {
579             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
580             return QBrush();
581         }
582         qreal cx = rx.cap(1).toDouble();
583         qreal cy = rx.cap(2).toDouble();
584         qreal angle = rx.cap(3).toDouble();
585         QGradientStops stops = parseGradientStops(rx.cap(4).trimmed());
586         if (!stops.count()) {
587             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
588             return QBrush();
589         }
590         QConicalGradient gradient(cx, cy, angle);
591         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
592         gradient.setStops(stops);
593         if (ok)
594             *ok = true;
595         return QBrush(gradient);
596     }
597     else if (str.startsWith("qradialgradient")) {
598         static const QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
599         static const QRegExp rx(QString("qradialgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*radius:%1,\\s*fx:%1,\\s*fy:%1,(.+)\\)").arg(rxFloat));
600         if (!rx.exactMatch(str)) {
601             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
602             return QBrush();
603         }
604         qreal cx = rx.cap(1).toDouble();
605         qreal cy = rx.cap(2).toDouble();
606         qreal radius = rx.cap(3).toDouble();
607         qreal fx = rx.cap(4).toDouble();
608         qreal fy = rx.cap(5).toDouble();
609         QGradientStops stops = parseGradientStops(rx.cap(6).trimmed());
610         if (!stops.count()) {
611             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
612             return QBrush();
613         }
614         QRadialGradient gradient(cx, cy, radius, fx, fy);
615         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
616         gradient.setStops(stops);
617         if (ok)
618             *ok = true;
619         return QBrush(gradient);
620     }
621
622     return QBrush();
623 }
624
625
626 QColor QssParser::parseColor(const QString &str)
627 {
628     if (str.startsWith("rgba")) {
629         ColorTuple tuple = parseColorTuple(str.mid(4));
630         if (tuple.count() == 4)
631             return QColor(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
632     }
633     else if (str.startsWith("rgb")) {
634         ColorTuple tuple = parseColorTuple(str.mid(3));
635         if (tuple.count() == 3)
636             return QColor(tuple.at(0), tuple.at(1), tuple.at(2));
637     }
638     else if (str.startsWith("hsva")) {
639         ColorTuple tuple = parseColorTuple(str.mid(4));
640         if (tuple.count() == 4) {
641             QColor c;
642             c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
643             return c;
644         }
645     }
646     else if (str.startsWith("hsv")) {
647         ColorTuple tuple = parseColorTuple(str.mid(3));
648         if (tuple.count() == 3) {
649             QColor c;
650             c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2));
651             return c;
652         }
653     }
654     else {
655         static const QRegExp rx("#?[0-9A-Fa-z]+");
656         if (rx.exactMatch(str))
657             return QColor(str);
658     }
659     return QColor();
660 }
661
662
663 // get a list of comma-separated int values or percentages (rel to 0-255)
664 QssParser::ColorTuple QssParser::parseColorTuple(const QString &str)
665 {
666     ColorTuple result;
667     static const QRegExp rx("\\(((\\s*[0-9]{1,3}%?\\s*)(,\\s*[0-9]{1,3}%?\\s*)*)\\)");
668     if (!rx.exactMatch(str.trimmed())) {
669         return ColorTuple();
670     }
671     QStringList values = rx.cap(1).split(',');
672     foreach(QString v, values) {
673         qreal val;
674         bool perc = false;
675         bool ok;
676         v = v.trimmed();
677         if (v.endsWith('%')) {
678             perc = true;
679             v.chop(1);
680         }
681         val = (qreal)v.toUInt(&ok);
682         if (!ok)
683             return ColorTuple();
684         if (perc)
685             val = 255 * val/100;
686         result.append(val);
687     }
688     return result;
689 }
690
691
692 QGradientStops QssParser::parseGradientStops(const QString &str_)
693 {
694     QString str = str_;
695     QGradientStops result;
696     static const QString rxFloat("(0?\\.[0-9]+|[01])"); // values between 0 and 1
697     static const QRegExp rx(QString("\\s*,?\\s*stop:\\s*(%1)\\s+([^:]+)(,\\s*stop:|$)").arg(rxFloat));
698     int idx;
699     while ((idx = rx.indexIn(str)) == 0) {
700         qreal x = rx.cap(1).toDouble();
701         QColor c = parseColor(rx.cap(3));
702         if (!c.isValid())
703             return QGradientStops();
704         result << QGradientStop(x, c);
705         str.remove(0, rx.matchedLength() - rx.cap(4).length());
706     }
707     if (!str.trimmed().isEmpty())
708         return QGradientStops();
709
710     return result;
711 }
712
713
714 /******** Font Properties ********/
715
716 void QssParser::parseFont(const QString &value, QTextCharFormat *format)
717 {
718     static const QRegExp rx("((?:(?:normal|italic|oblique|underline|strikethrough|bold|100|200|300|400|500|600|700|800|900) ){0,2}) ?(\\d+)(pt|px)? \"(.*)\"");
719     if (!rx.exactMatch(value)) {
720         qWarning() << Q_FUNC_INFO << tr("Invalid font specification: %1").arg(value);
721         return;
722     }
723     format->setFontItalic(false);
724     format->setFontWeight(QFont::Normal);
725     QStringList proplist = rx.cap(1).split(' ', QString::SkipEmptyParts);
726     foreach(QString prop, proplist) {
727         if (prop == "italic")
728             format->setFontItalic(true);
729         else if (prop == "underline")
730             format->setFontUnderline(true);
731         else if(prop == "oblique")
732             // Oblique is not a property supported by QTextCharFormat
733             format->setFontItalic(true);
734         else if (prop == "bold")
735             format->setFontWeight(QFont::Bold);
736         else { // number
737             int w = prop.toInt();
738             format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
739         }
740     }
741
742     if (rx.cap(3) == "px")
743         format->setProperty(QTextFormat::FontPixelSize, rx.cap(2).toInt());
744     else
745         format->setFontPointSize(rx.cap(2).toInt());
746
747     format->setFontFamily(rx.cap(4));
748 }
749
750
751 void QssParser::parseFontStyle(const QString &value, QTextCharFormat *format)
752 {
753     if (value == "normal")
754         format->setFontItalic(false);
755     else if (value == "italic")
756         format->setFontItalic(true);
757     else if (value == "underline")
758         format->setFontUnderline(true);
759     else if (value == "strikethrough")
760         format->setFontStrikeOut(true);
761     else if(value == "oblique")
762         // Oblique is not a property supported by QTextCharFormat
763         format->setFontItalic(true);
764     else {
765         qWarning() << Q_FUNC_INFO << tr("Invalid font style specification: %1").arg(value);
766     }
767 }
768
769
770 void QssParser::parseFontWeight(const QString &value, QTextCharFormat *format)
771 {
772     if (value == "normal")
773         format->setFontWeight(QFont::Normal);
774     else if (value == "bold")
775         format->setFontWeight(QFont::Bold);
776     else {
777         bool ok;
778         int w = value.toInt(&ok);
779         if (!ok) {
780             qWarning() << Q_FUNC_INFO << tr("Invalid font weight specification: %1").arg(value);
781             return;
782         }
783         format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
784     }
785 }
786
787
788 void QssParser::parseFontSize(const QString &value, QTextCharFormat *format)
789 {
790     static const QRegExp rx("(\\d+)(pt|px)");
791     if (!rx.exactMatch(value)) {
792         qWarning() << Q_FUNC_INFO << tr("Invalid font size specification: %1").arg(value);
793         return;
794     }
795     if (rx.cap(2) == "px")
796         format->setProperty(QTextFormat::FontPixelSize, rx.cap(1).toInt());
797     else
798         format->setFontPointSize(rx.cap(1).toInt());
799 }
800
801
802 void QssParser::parseFontFamily(const QString &value, QTextCharFormat *format)
803 {
804     QString family = value;
805     if (family.startsWith('"') && family.endsWith('"')) {
806         family = family.mid(1, family.length() - 2);
807     }
808     format->setFontFamily(family);
809 }