1 /***************************************************************************
2 * Copyright (C) 2005-2020 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 ***************************************************************************/
21 #include "qssparser.h"
26 #include <QApplication>
28 QssParser::QssParser()
30 _palette = QApplication::palette();
32 // Init palette color roles
33 _paletteColorRoles["alternate-base"] = QPalette::AlternateBase;
34 _paletteColorRoles["background"] = QPalette::Window;
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::WindowText;
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;
78 void QssParser::processStyleSheet(QString& ss)
83 // Remove C-style comments /* */ or //
84 static QRegExp commentRx(R"((//.*(\n|$)|/\*.*\*/))");
85 commentRx.setMinimal(true);
88 // Palette definitions first, so we can apply roles later on
89 static const QRegExp paletterx("(Palette[^{]*)\\{([^}]+)\\}");
91 while ((pos = paletterx.indexIn(ss, pos)) >= 0) {
92 parsePaletteBlock(paletterx.cap(1).trimmed(), paletterx.cap(2).trimmed());
93 ss.remove(pos, paletterx.matchedLength());
96 // Now we can parse the rest of our custom blocks
97 static const QRegExp blockrx("((?:ChatLine|ChatListItem|NickListItem)[^{]*)\\{([^}]+)\\}");
99 while ((pos = blockrx.indexIn(ss, pos)) >= 0) {
100 // qDebug() << blockrx.cap(1) << blockrx.cap(2);
101 QString declaration = blockrx.cap(1).trimmed();
102 QString contents = blockrx.cap(2).trimmed();
104 if (declaration.startsWith("ChatLine"))
105 parseChatLineBlock(declaration, contents);
106 else if (declaration.startsWith("ChatListItem") || declaration.startsWith("NickListItem"))
107 parseListItemBlock(declaration, contents);
109 // TODO: add moar here
111 ss.remove(pos, blockrx.matchedLength());
115 /******** Parse a whole block: declaration { contents } *******/
117 void QssParser::parseChatLineBlock(const QString& decl, const QString& contents)
119 UiStyle::FormatType fmtType;
120 UiStyle::MessageLabel label;
121 std::tie(fmtType, label) = parseFormatType(decl);
122 if (fmtType == UiStyle::FormatType::Invalid)
125 _formats[fmtType | label].merge(parseFormat(contents));
128 void QssParser::parseListItemBlock(const QString& decl, const QString& contents)
130 UiStyle::ItemFormatType fmtType = parseItemFormatType(decl);
131 if (fmtType == UiStyle::ItemFormatType::Invalid)
134 _listItemFormats[fmtType].merge(parseFormat(contents));
137 // Palette { ... } specifies the application palette
138 // ColorGroups can be specified like pseudo states, chaining is OR (contrary to normal CSS handling):
139 // Palette:inactive:disabled { ... } applies to both the Inactive and the Disabled state
140 void QssParser::parsePaletteBlock(const QString& decl, const QString& contents)
142 QList<QPalette::ColorGroup> colorGroups;
144 // Check if we want to apply this palette definition for particular ColorGroups
145 static const QRegExp rx("Palette((:(normal|active|inactive|disabled))*)");
146 if (!rx.exactMatch(decl)) {
147 qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
150 if (!rx.cap(1).isEmpty()) {
151 QStringList groups = rx.cap(1).split(':', QString::SkipEmptyParts);
152 foreach (QString g, groups) {
153 if ((g == "normal" || g == "active") && !colorGroups.contains(QPalette::Active))
154 colorGroups.append(QPalette::Active);
155 else if (g == "inactive" && !colorGroups.contains(QPalette::Inactive))
156 colorGroups.append(QPalette::Inactive);
157 else if (g == "disabled" && !colorGroups.contains(QPalette::Disabled))
158 colorGroups.append(QPalette::Disabled);
162 // Now let's go through the roles
163 foreach (QString line, contents.split(';', QString::SkipEmptyParts)) {
164 int idx = line.indexOf(':');
166 qWarning() << Q_FUNC_INFO << tr("Invalid palette role assignment: %1").arg(line.trimmed());
169 QString rolestr = line.left(idx).trimmed();
170 QString brushstr = line.mid(idx + 1).trimmed();
172 if (_paletteColorRoles.contains(rolestr)) {
173 QBrush brush = parseBrush(brushstr);
174 if (colorGroups.count()) {
175 foreach (QPalette::ColorGroup group, colorGroups)
176 _palette.setBrush(group, _paletteColorRoles.value(rolestr), brush);
179 _palette.setBrush(_paletteColorRoles.value(rolestr), brush);
181 else if (_uiStyleColorRoles.contains(rolestr)) {
182 _uiStylePalette[static_cast<int>(_uiStyleColorRoles.value(rolestr))] = parseBrush(brushstr);
185 qWarning() << Q_FUNC_INFO << tr("Unknown palette role name: %1").arg(rolestr);
189 /******** Determine format types from a block declaration ********/
191 std::pair<UiStyle::FormatType, UiStyle::MessageLabel> QssParser::parseFormatType(const QString& decl)
193 using FormatType = UiStyle::FormatType;
194 using MessageLabel = UiStyle::MessageLabel;
196 const std::pair<UiStyle::FormatType, UiStyle::MessageLabel> invalid{FormatType::Invalid, MessageLabel::None};
198 static const QRegExp rx(R"(ChatLine(?:::(\w+))?(?:#([\w\-]+))?(?:\[([=-,\"\w\s]+)\])?)");
199 // $1: subelement; $2: msgtype; $3: conditionals
200 if (!rx.exactMatch(decl)) {
201 qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
204 QString subElement = rx.cap(1);
205 QString msgType = rx.cap(2);
206 QString conditions = rx.cap(3);
208 FormatType fmtType{FormatType::Base};
209 MessageLabel label{MessageLabel::None};
211 // First determine the subelement
212 if (!subElement.isEmpty()) {
213 if (subElement == "timestamp")
214 fmtType |= FormatType::Timestamp;
215 else if (subElement == "sender")
216 fmtType |= FormatType::Sender;
217 else if (subElement == "nick")
218 fmtType |= FormatType::Nick;
219 else if (subElement == "contents")
220 fmtType |= FormatType::Contents;
221 else if (subElement == "hostmask")
222 fmtType |= FormatType::Hostmask;
223 else if (subElement == "modeflags")
224 fmtType |= FormatType::ModeFlags;
225 else if (subElement == "url")
226 fmtType |= FormatType::Url;
228 qWarning() << Q_FUNC_INFO << tr("Invalid subelement name in %1").arg(decl);
233 // Now, figure out the message type
234 if (!msgType.isEmpty()) {
235 if (msgType == "plain")
236 fmtType |= FormatType::PlainMsg;
237 else if (msgType == "notice")
238 fmtType |= FormatType::NoticeMsg;
239 else if (msgType == "action")
240 fmtType |= FormatType::ActionMsg;
241 else if (msgType == "nick")
242 fmtType |= FormatType::NickMsg;
243 else if (msgType == "mode")
244 fmtType |= FormatType::ModeMsg;
245 else if (msgType == "join")
246 fmtType |= FormatType::JoinMsg;
247 else if (msgType == "part")
248 fmtType |= FormatType::PartMsg;
249 else if (msgType == "quit")
250 fmtType |= FormatType::QuitMsg;
251 else if (msgType == "kick")
252 fmtType |= FormatType::KickMsg;
253 else if (msgType == "kill")
254 fmtType |= FormatType::KillMsg;
255 else if (msgType == "server")
256 fmtType |= FormatType::ServerMsg;
257 else if (msgType == "info")
258 fmtType |= FormatType::InfoMsg;
259 else if (msgType == "error")
260 fmtType |= FormatType::ErrorMsg;
261 else if (msgType == "daychange")
262 fmtType |= FormatType::DayChangeMsg;
263 else if (msgType == "topic")
264 fmtType |= FormatType::TopicMsg;
265 else if (msgType == "netsplit-join")
266 fmtType |= FormatType::NetsplitJoinMsg;
267 else if (msgType == "netsplit-quit")
268 fmtType |= FormatType::NetsplitQuitMsg;
269 else if (msgType == "invite")
270 fmtType |= FormatType::InviteMsg;
272 qWarning() << Q_FUNC_INFO << tr("Invalid message type in %1").arg(decl);
276 // Next up: conditional (formats, labels, nickhash)
277 static const QRegExp condRx(R"lit(\s*([\w\-]+)\s*=\s*"(\w+)"\s*)lit");
278 if (!conditions.isEmpty()) {
279 foreach (const QString& cond, conditions.split(',', QString::SkipEmptyParts)) {
280 if (!condRx.exactMatch(cond)) {
281 qWarning() << Q_FUNC_INFO << tr("Invalid condition %1").arg(cond);
284 QString condName = condRx.cap(1);
285 QString condValue = condRx.cap(2);
286 if (condName == "label") {
287 if (condValue == "highlight")
288 label |= MessageLabel::Highlight;
289 else if (condValue == "selected")
290 label |= MessageLabel::Selected;
291 else if (condValue == "hovered")
292 label |= MessageLabel::Hovered;
294 qWarning() << Q_FUNC_INFO << tr("Invalid message label: %1").arg(condValue);
298 else if (condName == "sender") {
299 if (condValue == "self")
300 label |= MessageLabel::OwnMsg; // sender="self" is actually treated as a label
303 quint32 val = condValue.toUInt(&ok, 16);
305 qWarning() << Q_FUNC_INFO << tr("Invalid senderhash specification: %1").arg(condValue);
309 qWarning() << Q_FUNC_INFO << tr("Senderhash can be at most \"0x0f\"!");
312 label |= static_cast<MessageLabel>(++val << 16);
315 else if (condName == "format") {
316 if (condValue == "bold")
317 fmtType |= FormatType::Bold;
318 else if (condValue == "italic")
319 fmtType |= FormatType::Italic;
320 else if (condValue == "underline")
321 fmtType |= FormatType::Underline;
322 else if (condValue == "strikethrough")
323 fmtType |= FormatType::Strikethrough;
325 qWarning() << Q_FUNC_INFO << tr("Invalid format name: %1").arg(condValue);
329 else if (condName == "fg-color" || condName == "bg-color") {
331 quint32 col = condValue.toUInt(&ok, 16);
332 if (!ok || col > 0x0f) {
333 qWarning() << Q_FUNC_INFO << tr("Illegal IRC color specification (must be between 00 and 0f): %1").arg(condValue);
336 if (condName == "fg-color")
337 fmtType |= 0x00400000 | (col << 24);
339 fmtType |= 0x00800000 | (col << 28);
342 qWarning() << Q_FUNC_INFO << tr("Unhandled condition: %1").arg(condName);
348 return std::make_pair(fmtType, label);
351 // FIXME: Code duplication
352 UiStyle::ItemFormatType QssParser::parseItemFormatType(const QString& decl)
354 using ItemFormatType = UiStyle::ItemFormatType;
356 static const QRegExp rx(R"((Chat|Nick)ListItem(?:\[([=-,\"\w\s]+)\])?)");
357 // $1: item type; $2: properties
358 if (!rx.exactMatch(decl)) {
359 qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
360 return ItemFormatType::Invalid;
362 QString mainItemType = rx.cap(1);
363 QString properties = rx.cap(2);
365 ItemFormatType fmtType{ItemFormatType::None};
367 // Next up: properties
369 if (!properties.isEmpty()) {
370 QHash<QString, QString> props;
371 static const QRegExp propRx(R"lit(\s*([\w\-]+)\s*=\s*"([\w\-]+)"\s*)lit");
372 foreach (const QString& prop, properties.split(',', QString::SkipEmptyParts)) {
373 if (!propRx.exactMatch(prop)) {
374 qWarning() << Q_FUNC_INFO << tr("Invalid proplist %1").arg(prop);
375 return ItemFormatType::Invalid;
377 props[propRx.cap(1)] = propRx.cap(2);
379 type = props.value("type");
380 state = props.value("state");
383 if (mainItemType == "Chat") {
384 fmtType |= ItemFormatType::BufferViewItem;
385 if (!type.isEmpty()) {
386 if (type == "network")
387 fmtType |= ItemFormatType::NetworkItem;
388 else if (type == "channel")
389 fmtType |= ItemFormatType::ChannelBufferItem;
390 else if (type == "query")
391 fmtType |= ItemFormatType::QueryBufferItem;
393 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist item type %1").arg(type);
394 return ItemFormatType::Invalid;
397 if (!state.isEmpty()) {
398 if (state == "inactive")
399 fmtType |= ItemFormatType::InactiveBuffer;
400 else if (state == "channel-event")
401 fmtType |= ItemFormatType::ActiveBuffer;
402 else if (state == "unread-message")
403 fmtType |= ItemFormatType::UnreadBuffer;
404 else if (state == "highlighted")
405 fmtType |= ItemFormatType::HighlightedBuffer;
406 else if (state == "away")
407 fmtType |= ItemFormatType::UserAway;
409 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist state %1").arg(state);
410 return ItemFormatType::Invalid;
415 fmtType |= ItemFormatType::NickViewItem;
416 if (!type.isEmpty()) {
417 if (type == "user") {
418 fmtType |= ItemFormatType::IrcUserItem;
420 fmtType |= ItemFormatType::UserAway;
422 else if (type == "category")
423 fmtType |= ItemFormatType::UserCategoryItem;
429 /******** Parse a whole format attribute block ********/
431 QTextCharFormat QssParser::parseFormat(const QString& qss)
433 QTextCharFormat format;
435 foreach (QString line, qss.split(';', QString::SkipEmptyParts)) {
436 int idx = line.indexOf(':');
438 qWarning() << Q_FUNC_INFO << tr("Invalid property declaration: %1").arg(line.trimmed());
441 QString property = line.left(idx).trimmed();
442 QString value = line.mid(idx + 1).simplified();
444 if (property == "background" || property == "background-color")
445 format.setBackground(parseBrush(value));
446 else if (property == "foreground" || property == "color")
447 format.setForeground(parseBrush(value));
449 // Color code overrides
450 else if (property == "allow-foreground-override") {
452 bool v = parseBoolean(value, &ok);
454 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowForegroundOverride), v);
456 else if (property == "allow-background-override") {
458 bool v = parseBoolean(value, &ok);
460 format.setProperty(static_cast<int>(UiStyle::FormatProperty::AllowBackgroundOverride), v);
463 // font-related properties
464 else if (property.startsWith("font")) {
465 if (property == "font")
466 parseFont(value, &format);
467 else if (property == "font-style")
468 parseFontStyle(value, &format);
469 else if (property == "font-weight")
470 parseFontWeight(value, &format);
471 else if (property == "font-size")
472 parseFontSize(value, &format);
473 else if (property == "font-family")
474 parseFontFamily(value, &format);
476 qWarning() << Q_FUNC_INFO << tr("Invalid font property: %1").arg(line);
482 qWarning() << Q_FUNC_INFO << tr("Unknown ChatLine property: %1").arg(property);
489 /******** Boolean value ********/
491 bool QssParser::parseBoolean(const QString& str, bool* ok) const
501 qWarning() << Q_FUNC_INFO << tr("Invalid boolean value: %1").arg(str);
507 /******** Brush ********/
509 QBrush QssParser::parseBrush(const QString& str, bool* ok)
513 QColor c = parseColor(str);
520 if (str.startsWith("palette")) { // Palette color role
521 // Does the palette follow the expected format? For example:
522 // palette(marker-line)
523 // palette ( system-color-0f )
525 // Match the palette marker, grabbing the name inside in case-sensitive manner
526 // palette\s*\(\s*([a-z-0-9]+)\s*\)
527 // palette Match the string 'palette'
528 // \s* Match any amount of whitespace
529 // \(, \) Match literal '(' or ')' marks
530 // (...+) Match contents between 1 and unlimited number of times
531 // [a-z-] Match any character from a-z, case sensitive
532 // [0-9] Match any digit from 0-9
533 // Note that '\' must be escaped as '\\'
534 // Helpful interactive website for debugging and explaining: https://regex101.com/
535 static const QRegExp rx(R"(palette\s*\(\s*([a-z-0-9]+)\s*\))");
536 if (!rx.exactMatch(str)) {
537 qWarning() << Q_FUNC_INFO << tr("Invalid palette color role specification: %1").arg(str);
540 if (_paletteColorRoles.contains(rx.cap(1)))
541 return QBrush(_palette.brush(_paletteColorRoles.value(rx.cap(1))));
542 if (_uiStyleColorRoles.contains(rx.cap(1)))
543 return QBrush(_uiStylePalette.at(static_cast<int>(_uiStyleColorRoles.value(rx.cap(1)))));
544 qWarning() << Q_FUNC_INFO << tr("Unknown palette color role: %1").arg(rx.cap(1));
547 else if (str.startsWith("qlineargradient")) {
548 static const QString rxFloat(R"(\s*(-?\s*[0-9]*\.?[0-9]+)\s*)");
549 static const QRegExp rx(QString(R"(qlineargradient\s*\(\s*x1:%1,\s*y1:%1,\s*x2:%1,\s*y2:%1,(.+)\))").arg(rxFloat));
550 if (!rx.exactMatch(str)) {
551 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
554 qreal x1 = rx.cap(1).toDouble();
555 qreal y1 = rx.cap(2).toDouble();
556 qreal x2 = rx.cap(3).toDouble();
557 qreal y2 = rx.cap(4).toDouble();
558 QGradientStops stops = parseGradientStops(rx.cap(5).trimmed());
559 if (!stops.count()) {
560 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
563 QLinearGradient gradient(x1, y1, x2, y2);
564 gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
565 gradient.setStops(stops);
568 return QBrush(gradient);
570 else if (str.startsWith("qconicalgradient")) {
571 static const QString rxFloat(R"(\s*(-?\s*[0-9]*\.?[0-9]+)\s*)");
572 static const QRegExp rx(QString(R"(qconicalgradient\s*\(\s*cx:%1,\s*cy:%1,\s*angle:%1,(.+)\))").arg(rxFloat));
573 if (!rx.exactMatch(str)) {
574 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
577 qreal cx = rx.cap(1).toDouble();
578 qreal cy = rx.cap(2).toDouble();
579 qreal angle = rx.cap(3).toDouble();
580 QGradientStops stops = parseGradientStops(rx.cap(4).trimmed());
581 if (!stops.count()) {
582 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
585 QConicalGradient gradient(cx, cy, angle);
586 gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
587 gradient.setStops(stops);
590 return QBrush(gradient);
592 else if (str.startsWith("qradialgradient")) {
593 static const QString rxFloat(R"(\s*(-?\s*[0-9]*\.?[0-9]+)\s*)");
594 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));
595 if (!rx.exactMatch(str)) {
596 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
599 qreal cx = rx.cap(1).toDouble();
600 qreal cy = rx.cap(2).toDouble();
601 qreal radius = rx.cap(3).toDouble();
602 qreal fx = rx.cap(4).toDouble();
603 qreal fy = rx.cap(5).toDouble();
604 QGradientStops stops = parseGradientStops(rx.cap(6).trimmed());
605 if (!stops.count()) {
606 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
609 QRadialGradient gradient(cx, cy, radius, fx, fy);
610 gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
611 gradient.setStops(stops);
614 return QBrush(gradient);
620 QColor QssParser::parseColor(const QString& str)
622 if (str.startsWith("rgba")) {
623 ColorTuple tuple = parseColorTuple(str.mid(4));
624 if (tuple.count() == 4)
625 return QColor(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3)); // NOLINT(modernize-return-braced-init-list)
627 else if (str.startsWith("rgb")) {
628 ColorTuple tuple = parseColorTuple(str.mid(3));
629 if (tuple.count() == 3)
630 return QColor(tuple.at(0), tuple.at(1), tuple.at(2));
632 else if (str.startsWith("hsva")) {
633 ColorTuple tuple = parseColorTuple(str.mid(4));
634 if (tuple.count() == 4) {
636 c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
640 else if (str.startsWith("hsv")) {
641 ColorTuple tuple = parseColorTuple(str.mid(3));
642 if (tuple.count() == 3) {
644 c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2));
649 static const QRegExp rx("#?[0-9A-Fa-z]+");
650 if (rx.exactMatch(str))
656 // get a list of comma-separated int values or percentages (rel to 0-255)
657 QssParser::ColorTuple QssParser::parseColorTuple(const QString& str)
660 static const QRegExp rx(R"(\(((\s*[0-9]{1,3}%?\s*)(,\s*[0-9]{1,3}%?\s*)*)\))");
661 if (!rx.exactMatch(str.trimmed())) {
664 QStringList values = rx.cap(1).split(',');
665 foreach (QString v, values) {
670 if (v.endsWith('%')) {
674 val = (qreal)v.toUInt(&ok);
678 val = 255 * val / 100;
684 QGradientStops QssParser::parseGradientStops(const QString& str_)
687 QGradientStops result;
688 static const QString rxFloat("(0?\\.[0-9]+|[01])"); // values between 0 and 1
689 static const QRegExp rx(QString(R"(\s*,?\s*stop:\s*(%1)\s+([^:]+)(,\s*stop:|$))").arg(rxFloat));
691 while ((idx = rx.indexIn(str)) == 0) {
692 qreal x = rx.cap(1).toDouble();
693 QColor c = parseColor(rx.cap(3));
695 return QGradientStops();
696 result << QGradientStop(x, c);
697 str.remove(0, rx.matchedLength() - rx.cap(4).length());
699 if (!str.trimmed().isEmpty())
700 return QGradientStops();
705 /******** Font Properties ********/
707 void QssParser::parseFont(const QString& value, QTextCharFormat* format)
709 static const QRegExp rx(
710 "((?:(?:normal|italic|oblique|underline|strikethrough|bold|100|200|300|400|500|600|700|800|900) ){0,2}) ?(\\d+)(pt|px)? \"(.*)\"");
711 if (!rx.exactMatch(value)) {
712 qWarning() << Q_FUNC_INFO << tr("Invalid font specification: %1").arg(value);
715 format->setFontItalic(false);
716 format->setFontUnderline(false);
717 format->setFontStrikeOut(false);
718 format->setFontWeight(QFont::Normal);
719 QStringList proplist = rx.cap(1).split(' ', QString::SkipEmptyParts);
720 foreach (QString prop, proplist) {
721 if (prop == "normal")
723 else if (prop == "italic")
724 format->setFontItalic(true);
725 else if (prop == "underline")
726 format->setFontUnderline(true);
727 else if (prop == "strikethrough")
728 format->setFontStrikeOut(true);
729 else if (prop == "oblique")
730 // Oblique is not a property supported by QTextCharFormat
731 format->setFontItalic(true);
732 else if (prop == "bold")
733 format->setFontWeight(QFont::Bold);
735 int w = prop.toInt();
736 format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
740 if (rx.cap(3) == "px")
741 format->setProperty(QTextFormat::FontPixelSize, rx.cap(2).toInt());
743 format->setFontPointSize(rx.cap(2).toInt());
745 format->setFontFamily(rx.cap(4));
748 void QssParser::parseFontStyle(const QString& value, QTextCharFormat* format)
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 == "strikethrough")
757 format->setFontStrikeOut(true);
758 else if (value == "oblique")
759 // Oblique is not a property supported by QTextCharFormat
760 format->setFontItalic(true);
762 qWarning() << Q_FUNC_INFO << tr("Invalid font style specification: %1").arg(value);
766 void QssParser::parseFontWeight(const QString& value, QTextCharFormat* format)
768 if (value == "normal")
769 format->setFontWeight(QFont::Normal);
770 else if (value == "bold")
771 format->setFontWeight(QFont::Bold);
774 int w = value.toInt(&ok);
776 qWarning() << Q_FUNC_INFO << tr("Invalid font weight specification: %1").arg(value);
779 format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
783 void QssParser::parseFontSize(const QString& value, QTextCharFormat* format)
785 static const QRegExp rx("(\\d+)(pt|px)");
786 if (!rx.exactMatch(value)) {
787 qWarning() << Q_FUNC_INFO << tr("Invalid font size specification: %1").arg(value);
790 if (rx.cap(2) == "px")
791 format->setProperty(QTextFormat::FontPixelSize, rx.cap(1).toInt());
793 format->setFontPointSize(rx.cap(1).toInt());
796 void QssParser::parseFontFamily(const QString& value, QTextCharFormat* format)
798 QString family = value;
799 if (family.startsWith('"') && family.endsWith('"')) {
800 family = family.mid(1, family.length() - 2);
802 format->setFontFamily(family);