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