07dab3a8e02b799037b36291c12f8fd2a1ae2bd5
[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 {
326                     qWarning() << Q_FUNC_INFO << tr("Invalid format name: %1").arg(condValue);
327                     return invalid;
328                 }
329             }
330             else if (condName == "fg-color" || condName == "bg-color") {
331                 bool ok;
332                 quint32 col = condValue.toUInt(&ok, 16);
333                 if (!ok || col > 0x0f) {
334                     qWarning() << Q_FUNC_INFO << tr("Illegal IRC color specification (must be between 00 and 0f): %1").arg(condValue);
335                     return invalid;
336                 }
337                 if (condName == "fg-color")
338                     fmtType |= 0x00400000 | (col << 24);
339                 else
340                     fmtType |= 0x00800000 | (col << 28);
341             }
342             else {
343                 qWarning() << Q_FUNC_INFO << tr("Unhandled condition: %1").arg(condName);
344                 return invalid;
345             }
346         }
347     }
348
349     return std::make_pair(fmtType, label);
350 }
351
352
353 // FIXME: Code duplication
354 UiStyle::ItemFormatType QssParser::parseItemFormatType(const QString &decl)
355 {
356     using ItemFormatType = UiStyle::ItemFormatType;
357
358     static const QRegExp rx("(Chat|Nick)ListItem(?:\\[([=-,\\\"\\w\\s]+)\\])?");
359     // $1: item type; $2: properties
360     if (!rx.exactMatch(decl)) {
361         qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
362         return ItemFormatType::Invalid;
363     }
364     QString mainItemType = rx.cap(1);
365     QString properties = rx.cap(2);
366
367     ItemFormatType fmtType{ItemFormatType::None};
368
369     // Next up: properties
370     QString type, state;
371     if (!properties.isEmpty()) {
372         QHash<QString, QString> props;
373         static const QRegExp propRx("\\s*([\\w\\-]+)\\s*=\\s*\"([\\w\\-]+)\"\\s*");
374         foreach(const QString &prop, properties.split(',', QString::SkipEmptyParts)) {
375             if (!propRx.exactMatch(prop)) {
376                 qWarning() << Q_FUNC_INFO << tr("Invalid proplist %1").arg(prop);
377                 return ItemFormatType::Invalid;
378             }
379             props[propRx.cap(1)] = propRx.cap(2);
380         }
381         type = props.value("type");
382         state = props.value("state");
383     }
384
385     if (mainItemType == "Chat") {
386         fmtType |= ItemFormatType::BufferViewItem;
387         if (!type.isEmpty()) {
388             if (type == "network")
389                 fmtType |= ItemFormatType::NetworkItem;
390             else if (type == "channel")
391                 fmtType |= ItemFormatType::ChannelBufferItem;
392             else if (type == "query")
393                 fmtType |= ItemFormatType::QueryBufferItem;
394             else {
395                 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist item type %1").arg(type);
396                 return ItemFormatType::Invalid;
397             }
398         }
399         if (!state.isEmpty()) {
400             if (state == "inactive")
401                 fmtType |= ItemFormatType::InactiveBuffer;
402             else if (state == "channel-event")
403                 fmtType |= ItemFormatType::ActiveBuffer;
404             else if (state == "unread-message")
405                 fmtType |= ItemFormatType::UnreadBuffer;
406             else if (state == "highlighted")
407                 fmtType |= ItemFormatType::HighlightedBuffer;
408             else if (state == "away")
409                 fmtType |= ItemFormatType::UserAway;
410             else {
411                 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist state %1").arg(state);
412                 return ItemFormatType::Invalid;
413             }
414         }
415     }
416     else { // NickList
417         fmtType |= ItemFormatType::NickViewItem;
418         if (!type.isEmpty()) {
419             if (type == "user") {
420                 fmtType |= ItemFormatType::IrcUserItem;
421                 if (state == "away")
422                     fmtType |= ItemFormatType::UserAway;
423             }
424             else if (type == "category")
425                 fmtType |= ItemFormatType::UserCategoryItem;
426         }
427     }
428     return fmtType;
429 }
430
431
432 /******** Parse a whole format attribute block ********/
433
434 QTextCharFormat QssParser::parseFormat(const QString &qss)
435 {
436     QTextCharFormat format;
437
438     foreach(QString line, qss.split(';', QString::SkipEmptyParts)) {
439         int idx = line.indexOf(':');
440         if (idx <= 0) {
441             qWarning() << Q_FUNC_INFO << tr("Invalid property declaration: %1").arg(line.trimmed());
442             continue;
443         }
444         QString property = line.left(idx).trimmed();
445         QString value = line.mid(idx + 1).simplified();
446
447         if (property == "background" || property == "background-color")
448             format.setBackground(parseBrush(value));
449         else if (property == "foreground" || property == "color")
450             format.setForeground(parseBrush(value));
451
452         // Color code overrides
453         else if (property == "allow-foreground-override") {
454             bool ok;
455             bool v = parseBoolean(value, &ok);
456             if (ok)
457                 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowForegroundOverride), v);
458         }
459         else if (property == "allow-background-override") {
460             bool ok;
461             bool v = parseBoolean(value, &ok);
462             if (ok)
463                 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowBackgroundOverride), v);
464         }
465
466         // font-related properties
467         else if (property.startsWith("font")) {
468             if (property == "font")
469                 parseFont(value, &format);
470             else if (property == "font-style")
471                 parseFontStyle(value, &format);
472             else if (property == "font-weight")
473                 parseFontWeight(value, &format);
474             else if (property == "font-size")
475                 parseFontSize(value, &format);
476             else if (property == "font-family")
477                 parseFontFamily(value, &format);
478             else {
479                 qWarning() << Q_FUNC_INFO << tr("Invalid font property: %1").arg(line);
480                 continue;
481             }
482         }
483
484         else {
485             qWarning() << Q_FUNC_INFO << tr("Unknown ChatLine property: %1").arg(property);
486         }
487     }
488
489     return format;
490 }
491
492 /******** Boolean value ********/
493
494 bool QssParser::parseBoolean(const QString &str, bool *ok) const
495 {
496     if (ok)
497         *ok = true;
498
499     if (str == "true")
500         return true;
501     if (str == "false")
502         return false;
503
504     qWarning() << Q_FUNC_INFO << tr("Invalid boolean value: %1").arg(str);
505     if (ok)
506         *ok = false;
507     return false;
508 }
509
510 /******** Brush ********/
511
512 QBrush QssParser::parseBrush(const QString &str, bool *ok)
513 {
514     if (ok)
515         *ok = false;
516     QColor c = parseColor(str);
517     if (c.isValid()) {
518         if (ok)
519             *ok = true;
520         return QBrush(c);
521     }
522
523     if (str.startsWith("palette")) { // Palette color role
524         // Does the palette follow the expected format?  For example:
525         // palette(marker-line)
526         // palette    ( system-color-0f  )
527         //
528         // Match the palette marker, grabbing the name inside in  case-sensitive manner
529         //   palette\s*\(\s*([a-z-0-9]+)\s*\)
530         //   palette   Match the string 'palette'
531         //   \s*       Match any amount of whitespace
532         //   \(, \)    Match literal '(' or ')' marks
533         //   (...+)    Match contents between 1 and unlimited number of times
534         //   [a-z-]    Match any character from a-z, case sensitive
535         //   [0-9]     Match any digit from 0-9
536         // Note that '\' must be escaped as '\\'
537         // Helpful interactive website for debugging and explaining:  https://regex101.com/
538         static const QRegExp rx("palette\\s*\\(\\s*([a-z-0-9]+)\\s*\\)");
539         if (!rx.exactMatch(str)) {
540             qWarning() << Q_FUNC_INFO << tr("Invalid palette color role specification: %1").arg(str);
541             return QBrush();
542         }
543         if (_paletteColorRoles.contains(rx.cap(1)))
544             return QBrush(_palette.brush(_paletteColorRoles.value(rx.cap(1))));
545         if (_uiStyleColorRoles.contains(rx.cap(1)))
546             return QBrush(_uiStylePalette.at(static_cast<int>(_uiStyleColorRoles.value(rx.cap(1)))));
547         qWarning() << Q_FUNC_INFO << tr("Unknown palette color role: %1").arg(rx.cap(1));
548         return QBrush();
549     }
550     else if (str.startsWith("qlineargradient")) {
551         static const QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
552         static const QRegExp rx(QString("qlineargradient\\s*\\(\\s*x1:%1,\\s*y1:%1,\\s*x2:%1,\\s*y2:%1,(.+)\\)").arg(rxFloat));
553         if (!rx.exactMatch(str)) {
554             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
555             return QBrush();
556         }
557         qreal x1 = rx.cap(1).toDouble();
558         qreal y1 = rx.cap(2).toDouble();
559         qreal x2 = rx.cap(3).toDouble();
560         qreal y2 = rx.cap(4).toDouble();
561         QGradientStops stops = parseGradientStops(rx.cap(5).trimmed());
562         if (!stops.count()) {
563             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
564             return QBrush();
565         }
566         QLinearGradient gradient(x1, y1, x2, y2);
567         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
568         gradient.setStops(stops);
569         if (ok)
570             *ok = true;
571         return QBrush(gradient);
572     }
573     else if (str.startsWith("qconicalgradient")) {
574         static const QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
575         static const QRegExp rx(QString("qconicalgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*angle:%1,(.+)\\)").arg(rxFloat));
576         if (!rx.exactMatch(str)) {
577             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
578             return QBrush();
579         }
580         qreal cx = rx.cap(1).toDouble();
581         qreal cy = rx.cap(2).toDouble();
582         qreal angle = rx.cap(3).toDouble();
583         QGradientStops stops = parseGradientStops(rx.cap(4).trimmed());
584         if (!stops.count()) {
585             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
586             return QBrush();
587         }
588         QConicalGradient gradient(cx, cy, angle);
589         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
590         gradient.setStops(stops);
591         if (ok)
592             *ok = true;
593         return QBrush(gradient);
594     }
595     else if (str.startsWith("qradialgradient")) {
596         static const QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
597         static const QRegExp rx(QString("qradialgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*radius:%1,\\s*fx:%1,\\s*fy:%1,(.+)\\)").arg(rxFloat));
598         if (!rx.exactMatch(str)) {
599             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
600             return QBrush();
601         }
602         qreal cx = rx.cap(1).toDouble();
603         qreal cy = rx.cap(2).toDouble();
604         qreal radius = rx.cap(3).toDouble();
605         qreal fx = rx.cap(4).toDouble();
606         qreal fy = rx.cap(5).toDouble();
607         QGradientStops stops = parseGradientStops(rx.cap(6).trimmed());
608         if (!stops.count()) {
609             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
610             return QBrush();
611         }
612         QRadialGradient gradient(cx, cy, radius, fx, fy);
613         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
614         gradient.setStops(stops);
615         if (ok)
616             *ok = true;
617         return QBrush(gradient);
618     }
619
620     return QBrush();
621 }
622
623
624 QColor QssParser::parseColor(const QString &str)
625 {
626     if (str.startsWith("rgba")) {
627         ColorTuple tuple = parseColorTuple(str.mid(4));
628         if (tuple.count() == 4)
629             return QColor(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
630     }
631     else if (str.startsWith("rgb")) {
632         ColorTuple tuple = parseColorTuple(str.mid(3));
633         if (tuple.count() == 3)
634             return QColor(tuple.at(0), tuple.at(1), tuple.at(2));
635     }
636     else if (str.startsWith("hsva")) {
637         ColorTuple tuple = parseColorTuple(str.mid(4));
638         if (tuple.count() == 4) {
639             QColor c;
640             c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
641             return c;
642         }
643     }
644     else if (str.startsWith("hsv")) {
645         ColorTuple tuple = parseColorTuple(str.mid(3));
646         if (tuple.count() == 3) {
647             QColor c;
648             c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2));
649             return c;
650         }
651     }
652     else {
653         static const QRegExp rx("#?[0-9A-Fa-z]+");
654         if (rx.exactMatch(str))
655             return QColor(str);
656     }
657     return QColor();
658 }
659
660
661 // get a list of comma-separated int values or percentages (rel to 0-255)
662 QssParser::ColorTuple QssParser::parseColorTuple(const QString &str)
663 {
664     ColorTuple result;
665     static const QRegExp rx("\\(((\\s*[0-9]{1,3}%?\\s*)(,\\s*[0-9]{1,3}%?\\s*)*)\\)");
666     if (!rx.exactMatch(str.trimmed())) {
667         return ColorTuple();
668     }
669     QStringList values = rx.cap(1).split(',');
670     foreach(QString v, values) {
671         qreal val;
672         bool perc = false;
673         bool ok;
674         v = v.trimmed();
675         if (v.endsWith('%')) {
676             perc = true;
677             v.chop(1);
678         }
679         val = (qreal)v.toUInt(&ok);
680         if (!ok)
681             return ColorTuple();
682         if (perc)
683             val = 255 * val/100;
684         result.append(val);
685     }
686     return result;
687 }
688
689
690 QGradientStops QssParser::parseGradientStops(const QString &str_)
691 {
692     QString str = str_;
693     QGradientStops result;
694     static const QString rxFloat("(0?\\.[0-9]+|[01])"); // values between 0 and 1
695     static const QRegExp rx(QString("\\s*,?\\s*stop:\\s*(%1)\\s+([^:]+)(,\\s*stop:|$)").arg(rxFloat));
696     int idx;
697     while ((idx = rx.indexIn(str)) == 0) {
698         qreal x = rx.cap(1).toDouble();
699         QColor c = parseColor(rx.cap(3));
700         if (!c.isValid())
701             return QGradientStops();
702         result << QGradientStop(x, c);
703         str.remove(0, rx.matchedLength() - rx.cap(4).length());
704     }
705     if (!str.trimmed().isEmpty())
706         return QGradientStops();
707
708     return result;
709 }
710
711
712 /******** Font Properties ********/
713
714 void QssParser::parseFont(const QString &value, QTextCharFormat *format)
715 {
716     static const QRegExp rx("((?:(?:normal|italic|oblique|underline|bold|100|200|300|400|500|600|700|800|900) ){0,2}) ?(\\d+)(pt|px)? \"(.*)\"");
717     if (!rx.exactMatch(value)) {
718         qWarning() << Q_FUNC_INFO << tr("Invalid font specification: %1").arg(value);
719         return;
720     }
721     format->setFontItalic(false);
722     format->setFontWeight(QFont::Normal);
723     QStringList proplist = rx.cap(1).split(' ', QString::SkipEmptyParts);
724     foreach(QString prop, proplist) {
725         if (prop == "italic")
726             format->setFontItalic(true);
727         else if (prop == "underline")
728             format->setFontUnderline(true);
729         //else if(prop == "oblique")
730         //  format->setStyle(QFont::StyleOblique);
731         else if (prop == "bold")
732             format->setFontWeight(QFont::Bold);
733         else { // number
734             int w = prop.toInt();
735             format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
736         }
737     }
738
739     if (rx.cap(3) == "px")
740         format->setProperty(QTextFormat::FontPixelSize, rx.cap(2).toInt());
741     else
742         format->setFontPointSize(rx.cap(2).toInt());
743
744     format->setFontFamily(rx.cap(4));
745 }
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 == "oblique")
757     //  format->setStyle(QFont::StyleOblique);
758     else {
759         qWarning() << Q_FUNC_INFO << tr("Invalid font style specification: %1").arg(value);
760     }
761 }
762
763
764 void QssParser::parseFontWeight(const QString &value, QTextCharFormat *format)
765 {
766     if (value == "normal")
767         format->setFontWeight(QFont::Normal);
768     else if (value == "bold")
769         format->setFontWeight(QFont::Bold);
770     else {
771         bool ok;
772         int w = value.toInt(&ok);
773         if (!ok) {
774             qWarning() << Q_FUNC_INFO << tr("Invalid font weight specification: %1").arg(value);
775             return;
776         }
777         format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
778     }
779 }
780
781
782 void QssParser::parseFontSize(const QString &value, QTextCharFormat *format)
783 {
784     static const QRegExp rx("(\\d+)(pt|px)");
785     if (!rx.exactMatch(value)) {
786         qWarning() << Q_FUNC_INFO << tr("Invalid font size specification: %1").arg(value);
787         return;
788     }
789     if (rx.cap(2) == "px")
790         format->setProperty(QTextFormat::FontPixelSize, rx.cap(1).toInt());
791     else
792         format->setFontPointSize(rx.cap(1).toInt());
793 }
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 }