1 /***************************************************************************
2 * Copyright (C) 2005-2018 by the Quassel Project *
3 * devel@quassel-irc.org *
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. *
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. *
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 ***************************************************************************/
24 #include <QApplication>
26 #include "qssparser.h"
28 QssParser::QssParser()
30 _palette = QApplication::palette();
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;
55 _uiStylePalette = QVector<QBrush>(static_cast<int>(UiStyle::ColorRole::NumRoles), QBrush());
57 _uiStyleColorRoles["marker-line"] = UiStyle::ColorRole::MarkerLine;
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;
79 void QssParser::processStyleSheet(QString &ss)
84 // Remove C-style comments /* */ or //
85 static QRegExp commentRx(R"((//.*(\n|$)|/\*.*\*/))");
86 commentRx.setMinimal(true);
89 // Palette definitions first, so we can apply roles later on
90 static const QRegExp paletterx("(Palette[^{]*)\\{([^}]+)\\}");
92 while ((pos = paletterx.indexIn(ss, pos)) >= 0) {
93 parsePaletteBlock(paletterx.cap(1).trimmed(), paletterx.cap(2).trimmed());
94 ss.remove(pos, paletterx.matchedLength());
97 // Now we can parse the rest of our custom blocks
98 static const QRegExp blockrx("((?:ChatLine|ChatListItem|NickListItem)[^{]*)\\{([^}]+)\\}");
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();
105 if (declaration.startsWith("ChatLine"))
106 parseChatLineBlock(declaration, contents);
107 else if (declaration.startsWith("ChatListItem") || declaration.startsWith("NickListItem"))
108 parseListItemBlock(declaration, contents);
110 // TODO: add moar here
112 ss.remove(pos, blockrx.matchedLength());
117 /******** Parse a whole block: declaration { contents } *******/
119 void QssParser::parseChatLineBlock(const QString &decl, const QString &contents)
121 UiStyle::FormatType fmtType;
122 UiStyle::MessageLabel label;
123 std::tie(fmtType, label) = parseFormatType(decl);
124 if (fmtType == UiStyle::FormatType::Invalid)
127 _formats[fmtType|label].merge(parseFormat(contents));
131 void QssParser::parseListItemBlock(const QString &decl, const QString &contents)
133 UiStyle::ItemFormatType fmtType = parseItemFormatType(decl);
134 if (fmtType == UiStyle::ItemFormatType::Invalid)
137 _listItemFormats[fmtType].merge(parseFormat(contents));
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)
146 QList<QPalette::ColorGroup> colorGroups;
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);
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);
166 // Now let's go through the roles
167 foreach(QString line, contents.split(';', QString::SkipEmptyParts)) {
168 int idx = line.indexOf(':');
170 qWarning() << Q_FUNC_INFO << tr("Invalid palette role assignment: %1").arg(line.trimmed());
173 QString rolestr = line.left(idx).trimmed();
174 QString brushstr = line.mid(idx + 1).trimmed();
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);
183 _palette.setBrush(_paletteColorRoles.value(rolestr), brush);
185 else if (_uiStyleColorRoles.contains(rolestr)) {
186 _uiStylePalette[static_cast<int>(_uiStyleColorRoles.value(rolestr))] = parseBrush(brushstr);
189 qWarning() << Q_FUNC_INFO << tr("Unknown palette role name: %1").arg(rolestr);
194 /******** Determine format types from a block declaration ********/
196 std::pair<UiStyle::FormatType, UiStyle::MessageLabel> QssParser::parseFormatType(const QString &decl)
198 using FormatType = UiStyle::FormatType;
199 using MessageLabel = UiStyle::MessageLabel;
201 const std::pair<UiStyle::FormatType, UiStyle::MessageLabel> invalid{FormatType::Invalid, MessageLabel::None};
203 static const QRegExp rx(R"(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);
209 QString subElement = rx.cap(1);
210 QString msgType = rx.cap(2);
211 QString conditions = rx.cap(3);
213 FormatType fmtType{FormatType::Base};
214 MessageLabel label{MessageLabel::None};
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;
233 qWarning() << Q_FUNC_INFO << tr("Invalid subelement name in %1").arg(decl);
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;
277 qWarning() << Q_FUNC_INFO << tr("Invalid message type in %1").arg(decl);
281 // Next up: conditional (formats, labels, nickhash)
282 static const QRegExp condRx(R"lit(\s*([\w\-]+)\s*=\s*"(\w+)"\s*)lit");
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);
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 if (condValue == "hovered")
297 label |= MessageLabel::Hovered;
299 qWarning() << Q_FUNC_INFO << tr("Invalid message label: %1").arg(condValue);
303 else if (condName == "sender") {
304 if (condValue == "self")
305 label |= MessageLabel::OwnMsg; // sender="self" is actually treated as a label
308 quint32 val = condValue.toUInt(&ok, 16);
310 qWarning() << Q_FUNC_INFO << tr("Invalid senderhash specification: %1").arg(condValue);
314 qWarning() << Q_FUNC_INFO << tr("Senderhash can be at most \"0x0f\"!");
317 label |= static_cast<MessageLabel>(++val << 16);
320 else if (condName == "format") {
321 if (condValue == "bold")
322 fmtType |= FormatType::Bold;
323 else if (condValue == "italic")
324 fmtType |= FormatType::Italic;
325 else if (condValue == "underline")
326 fmtType |= FormatType::Underline;
327 else if (condValue == "strikethrough")
328 fmtType |= FormatType::Strikethrough;
330 qWarning() << Q_FUNC_INFO << tr("Invalid format name: %1").arg(condValue);
334 else if (condName == "fg-color" || condName == "bg-color") {
336 quint32 col = condValue.toUInt(&ok, 16);
337 if (!ok || col > 0x0f) {
338 qWarning() << Q_FUNC_INFO << tr("Illegal IRC color specification (must be between 00 and 0f): %1").arg(condValue);
341 if (condName == "fg-color")
342 fmtType |= 0x00400000 | (col << 24);
344 fmtType |= 0x00800000 | (col << 28);
347 qWarning() << Q_FUNC_INFO << tr("Unhandled condition: %1").arg(condName);
353 return std::make_pair(fmtType, label);
357 // FIXME: Code duplication
358 UiStyle::ItemFormatType QssParser::parseItemFormatType(const QString &decl)
360 using ItemFormatType = UiStyle::ItemFormatType;
362 static const QRegExp rx(R"((Chat|Nick)ListItem(?:\[([=-,\"\w\s]+)\])?)");
363 // $1: item type; $2: properties
364 if (!rx.exactMatch(decl)) {
365 qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
366 return ItemFormatType::Invalid;
368 QString mainItemType = rx.cap(1);
369 QString properties = rx.cap(2);
371 ItemFormatType fmtType{ItemFormatType::None};
373 // Next up: properties
375 if (!properties.isEmpty()) {
376 QHash<QString, QString> props;
377 static const QRegExp propRx(R"lit(\s*([\w\-]+)\s*=\s*"([\w\-]+)"\s*)lit");
378 foreach(const QString &prop, properties.split(',', QString::SkipEmptyParts)) {
379 if (!propRx.exactMatch(prop)) {
380 qWarning() << Q_FUNC_INFO << tr("Invalid proplist %1").arg(prop);
381 return ItemFormatType::Invalid;
383 props[propRx.cap(1)] = propRx.cap(2);
385 type = props.value("type");
386 state = props.value("state");
389 if (mainItemType == "Chat") {
390 fmtType |= ItemFormatType::BufferViewItem;
391 if (!type.isEmpty()) {
392 if (type == "network")
393 fmtType |= ItemFormatType::NetworkItem;
394 else if (type == "channel")
395 fmtType |= ItemFormatType::ChannelBufferItem;
396 else if (type == "query")
397 fmtType |= ItemFormatType::QueryBufferItem;
399 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist item type %1").arg(type);
400 return ItemFormatType::Invalid;
403 if (!state.isEmpty()) {
404 if (state == "inactive")
405 fmtType |= ItemFormatType::InactiveBuffer;
406 else if (state == "channel-event")
407 fmtType |= ItemFormatType::ActiveBuffer;
408 else if (state == "unread-message")
409 fmtType |= ItemFormatType::UnreadBuffer;
410 else if (state == "highlighted")
411 fmtType |= ItemFormatType::HighlightedBuffer;
412 else if (state == "away")
413 fmtType |= ItemFormatType::UserAway;
415 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist state %1").arg(state);
416 return ItemFormatType::Invalid;
421 fmtType |= ItemFormatType::NickViewItem;
422 if (!type.isEmpty()) {
423 if (type == "user") {
424 fmtType |= ItemFormatType::IrcUserItem;
426 fmtType |= ItemFormatType::UserAway;
428 else if (type == "category")
429 fmtType |= ItemFormatType::UserCategoryItem;
436 /******** Parse a whole format attribute block ********/
438 QTextCharFormat QssParser::parseFormat(const QString &qss)
440 QTextCharFormat format;
442 foreach(QString line, qss.split(';', QString::SkipEmptyParts)) {
443 int idx = line.indexOf(':');
445 qWarning() << Q_FUNC_INFO << tr("Invalid property declaration: %1").arg(line.trimmed());
448 QString property = line.left(idx).trimmed();
449 QString value = line.mid(idx + 1).simplified();
451 if (property == "background" || property == "background-color")
452 format.setBackground(parseBrush(value));
453 else if (property == "foreground" || property == "color")
454 format.setForeground(parseBrush(value));
456 // Color code overrides
457 else if (property == "allow-foreground-override") {
459 bool v = parseBoolean(value, &ok);
461 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowForegroundOverride), v);
463 else if (property == "allow-background-override") {
465 bool v = parseBoolean(value, &ok);
467 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowBackgroundOverride), v);
470 // font-related properties
471 else if (property.startsWith("font")) {
472 if (property == "font")
473 parseFont(value, &format);
474 else if (property == "font-style")
475 parseFontStyle(value, &format);
476 else if (property == "font-weight")
477 parseFontWeight(value, &format);
478 else if (property == "font-size")
479 parseFontSize(value, &format);
480 else if (property == "font-family")
481 parseFontFamily(value, &format);
483 qWarning() << Q_FUNC_INFO << tr("Invalid font property: %1").arg(line);
489 qWarning() << Q_FUNC_INFO << tr("Unknown ChatLine property: %1").arg(property);
496 /******** Boolean value ********/
498 bool QssParser::parseBoolean(const QString &str, bool *ok) const
508 qWarning() << Q_FUNC_INFO << tr("Invalid boolean value: %1").arg(str);
514 /******** Brush ********/
516 QBrush QssParser::parseBrush(const QString &str, bool *ok)
520 QColor c = parseColor(str);
527 if (str.startsWith("palette")) { // Palette color role
528 // Does the palette follow the expected format? For example:
529 // palette(marker-line)
530 // palette ( system-color-0f )
532 // Match the palette marker, grabbing the name inside in case-sensitive manner
533 // palette\s*\(\s*([a-z-0-9]+)\s*\)
534 // palette Match the string 'palette'
535 // \s* Match any amount of whitespace
536 // \(, \) Match literal '(' or ')' marks
537 // (...+) Match contents between 1 and unlimited number of times
538 // [a-z-] Match any character from a-z, case sensitive
539 // [0-9] Match any digit from 0-9
540 // Note that '\' must be escaped as '\\'
541 // Helpful interactive website for debugging and explaining: https://regex101.com/
542 static const QRegExp rx(R"(palette\s*\(\s*([a-z-0-9]+)\s*\))");
543 if (!rx.exactMatch(str)) {
544 qWarning() << Q_FUNC_INFO << tr("Invalid palette color role specification: %1").arg(str);
547 if (_paletteColorRoles.contains(rx.cap(1)))
548 return QBrush(_palette.brush(_paletteColorRoles.value(rx.cap(1))));
549 if (_uiStyleColorRoles.contains(rx.cap(1)))
550 return QBrush(_uiStylePalette.at(static_cast<int>(_uiStyleColorRoles.value(rx.cap(1)))));
551 qWarning() << Q_FUNC_INFO << tr("Unknown palette color role: %1").arg(rx.cap(1));
554 else if (str.startsWith("qlineargradient")) {
555 static const QString rxFloat(R"(\s*(-?\s*[0-9]*\.?[0-9]+)\s*)");
556 static const QRegExp rx(QString(R"(qlineargradient\s*\(\s*x1:%1,\s*y1:%1,\s*x2:%1,\s*y2:%1,(.+)\))").arg(rxFloat));
557 if (!rx.exactMatch(str)) {
558 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
561 qreal x1 = rx.cap(1).toDouble();
562 qreal y1 = rx.cap(2).toDouble();
563 qreal x2 = rx.cap(3).toDouble();
564 qreal y2 = rx.cap(4).toDouble();
565 QGradientStops stops = parseGradientStops(rx.cap(5).trimmed());
566 if (!stops.count()) {
567 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
570 QLinearGradient gradient(x1, y1, x2, y2);
571 gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
572 gradient.setStops(stops);
575 return QBrush(gradient);
577 else if (str.startsWith("qconicalgradient")) {
578 static const QString rxFloat(R"(\s*(-?\s*[0-9]*\.?[0-9]+)\s*)");
579 static const QRegExp rx(QString(R"(qconicalgradient\s*\(\s*cx:%1,\s*cy:%1,\s*angle:%1,(.+)\))").arg(rxFloat));
580 if (!rx.exactMatch(str)) {
581 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
584 qreal cx = rx.cap(1).toDouble();
585 qreal cy = rx.cap(2).toDouble();
586 qreal angle = rx.cap(3).toDouble();
587 QGradientStops stops = parseGradientStops(rx.cap(4).trimmed());
588 if (!stops.count()) {
589 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
592 QConicalGradient gradient(cx, cy, angle);
593 gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
594 gradient.setStops(stops);
597 return QBrush(gradient);
599 else if (str.startsWith("qradialgradient")) {
600 static const QString rxFloat(R"(\s*(-?\s*[0-9]*\.?[0-9]+)\s*)");
601 static const QRegExp rx(QString(R"(qradialgradient\s*\(\s*cx:%1,\s*cy:%1,\s*radius:%1,\s*fx:%1,\s*fy:%1,(.+)\))").arg(rxFloat));
602 if (!rx.exactMatch(str)) {
603 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
606 qreal cx = rx.cap(1).toDouble();
607 qreal cy = rx.cap(2).toDouble();
608 qreal radius = rx.cap(3).toDouble();
609 qreal fx = rx.cap(4).toDouble();
610 qreal fy = rx.cap(5).toDouble();
611 QGradientStops stops = parseGradientStops(rx.cap(6).trimmed());
612 if (!stops.count()) {
613 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
616 QRadialGradient gradient(cx, cy, radius, fx, fy);
617 gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
618 gradient.setStops(stops);
621 return QBrush(gradient);
628 QColor QssParser::parseColor(const QString &str)
630 if (str.startsWith("rgba")) {
631 ColorTuple tuple = parseColorTuple(str.mid(4));
632 if (tuple.count() == 4)
633 return QColor(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3)); // NOLINT(modernize-return-braced-init-list)
635 else if (str.startsWith("rgb")) {
636 ColorTuple tuple = parseColorTuple(str.mid(3));
637 if (tuple.count() == 3)
638 return QColor(tuple.at(0), tuple.at(1), tuple.at(2));
640 else if (str.startsWith("hsva")) {
641 ColorTuple tuple = parseColorTuple(str.mid(4));
642 if (tuple.count() == 4) {
644 c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
648 else if (str.startsWith("hsv")) {
649 ColorTuple tuple = parseColorTuple(str.mid(3));
650 if (tuple.count() == 3) {
652 c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2));
657 static const QRegExp rx("#?[0-9A-Fa-z]+");
658 if (rx.exactMatch(str))
665 // get a list of comma-separated int values or percentages (rel to 0-255)
666 QssParser::ColorTuple QssParser::parseColorTuple(const QString &str)
669 static const QRegExp rx(R"(\(((\s*[0-9]{1,3}%?\s*)(,\s*[0-9]{1,3}%?\s*)*)\))");
670 if (!rx.exactMatch(str.trimmed())) {
673 QStringList values = rx.cap(1).split(',');
674 foreach(QString v, values) {
679 if (v.endsWith('%')) {
683 val = (qreal)v.toUInt(&ok);
694 QGradientStops QssParser::parseGradientStops(const QString &str_)
697 QGradientStops result;
698 static const QString rxFloat("(0?\\.[0-9]+|[01])"); // values between 0 and 1
699 static const QRegExp rx(QString(R"(\s*,?\s*stop:\s*(%1)\s+([^:]+)(,\s*stop:|$))").arg(rxFloat));
701 while ((idx = rx.indexIn(str)) == 0) {
702 qreal x = rx.cap(1).toDouble();
703 QColor c = parseColor(rx.cap(3));
705 return QGradientStops();
706 result << QGradientStop(x, c);
707 str.remove(0, rx.matchedLength() - rx.cap(4).length());
709 if (!str.trimmed().isEmpty())
710 return QGradientStops();
716 /******** Font Properties ********/
718 void QssParser::parseFont(const QString &value, QTextCharFormat *format)
720 static const QRegExp rx("((?:(?:normal|italic|oblique|underline|strikethrough|bold|100|200|300|400|500|600|700|800|900) ){0,2}) ?(\\d+)(pt|px)? \"(.*)\"");
721 if (!rx.exactMatch(value)) {
722 qWarning() << Q_FUNC_INFO << tr("Invalid font specification: %1").arg(value);
725 format->setFontItalic(false);
726 format->setFontUnderline(false);
727 format->setFontStrikeOut(false);
728 format->setFontWeight(QFont::Normal);
729 QStringList proplist = rx.cap(1).split(' ', QString::SkipEmptyParts);
730 foreach(QString prop, proplist) {
731 if (prop == "normal")
733 else if (prop == "italic")
734 format->setFontItalic(true);
735 else if (prop == "underline")
736 format->setFontUnderline(true);
737 else if (prop == "strikethrough")
738 format->setFontStrikeOut(true);
739 else if(prop == "oblique")
740 // Oblique is not a property supported by QTextCharFormat
741 format->setFontItalic(true);
742 else if (prop == "bold")
743 format->setFontWeight(QFont::Bold);
745 int w = prop.toInt();
746 format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
750 if (rx.cap(3) == "px")
751 format->setProperty(QTextFormat::FontPixelSize, rx.cap(2).toInt());
753 format->setFontPointSize(rx.cap(2).toInt());
755 format->setFontFamily(rx.cap(4));
759 void QssParser::parseFontStyle(const QString &value, QTextCharFormat *format)
761 if (value == "normal")
762 format->setFontItalic(false);
763 else if (value == "italic")
764 format->setFontItalic(true);
765 else if (value == "underline")
766 format->setFontUnderline(true);
767 else if (value == "strikethrough")
768 format->setFontStrikeOut(true);
769 else if(value == "oblique")
770 // Oblique is not a property supported by QTextCharFormat
771 format->setFontItalic(true);
773 qWarning() << Q_FUNC_INFO << tr("Invalid font style specification: %1").arg(value);
778 void QssParser::parseFontWeight(const QString &value, QTextCharFormat *format)
780 if (value == "normal")
781 format->setFontWeight(QFont::Normal);
782 else if (value == "bold")
783 format->setFontWeight(QFont::Bold);
786 int w = value.toInt(&ok);
788 qWarning() << Q_FUNC_INFO << tr("Invalid font weight specification: %1").arg(value);
791 format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
796 void QssParser::parseFontSize(const QString &value, QTextCharFormat *format)
798 static const QRegExp rx("(\\d+)(pt|px)");
799 if (!rx.exactMatch(value)) {
800 qWarning() << Q_FUNC_INFO << tr("Invalid font size specification: %1").arg(value);
803 if (rx.cap(2) == "px")
804 format->setProperty(QTextFormat::FontPixelSize, rx.cap(1).toInt());
806 format->setFontPointSize(rx.cap(1).toInt());
810 void QssParser::parseFontFamily(const QString &value, QTextCharFormat *format)
812 QString family = value;
813 if (family.startsWith('"') && family.endsWith('"')) {
814 family = family.mid(1, family.length() - 2);
816 format->setFontFamily(family);