NEW CSS-parser JKQTPCSSParser in order to parse e.g. \c linear-gradient() specifications in jkqtp_String2QBrushStyleExt()

NEW generic RegularExpression functions (jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt() )
This commit is contained in:
jkriege2 2024-01-21 22:12:15 +01:00
parent 11bafa52e4
commit eb7a068fb7
9 changed files with 1032 additions and 170 deletions

View File

@ -20,6 +20,11 @@ This group assembles a variety of mathematical tool functions that are used in d
Offers diverse function to convert different datatypes (e.g. double, int, diverse enums) to and from strings and for string manipulation.
\defgroup jkqtptools_css CSS Parsing Tools
\ingroup jkqtptools_jkqtcommon
Offers parser(s) for certain subsets of css, that are used e.g. in JKQTPlotter's styling INI files (e.g. for gradients)
\defgroup jkqtptools_qt Tools around Qt's clasess
\ingroup jkqtptools_jkqtcommon

View File

@ -30,7 +30,6 @@ This page lists several todos and wishes for future version of JKQTPlotter
<li>plot: elongated grid to left of tick labels</li>
<li>plot: refactor print preview/export preview code </li>
<li>sryling: better styling/more styling options for data-tooltips</li>
<li>styling: color gradients as fill-styles in style-INIs</li>
<li>user interactions: dialog(s) to edit x/y-range</li>
<li></li>
</ul></li>

View File

@ -28,8 +28,8 @@ Changes, compared to \ref page_whatsnew_V4_0_0 "v4.0.0" include:
<li>NEW/BREAKING: refactor CMake-Code, so static/dynamic switch is done via <code>BUILD_SHARED_LIBS</code>, which retires <code>JKQtPlotter_BUILD_STATIC_LIBS</code>, <code>JKQtPlotter_BUILD_SHARED_LIBS</code> and removes the capability to build static and shared libraries in one location (fixes issue #104)</li>
<li>NEW: prepareed library for CMake's <a href="https://cmake.org/cmake/help/latest/module/FetchContent.html">FetchContent</a>-API</li>
<li>NEW: the different sub-libraries JKQTPlotter, JKQTFastPlotter (DEPRECATED), JKQTMath, JKQTMathText can be activated/deactivated with CMake options JKQtPlotter_BUILD_LIB_JKQTPLOTTER, JKQtPlotter_BUILD_LIB_JKQTFASTPLOTTER, JKQtPlotter_BUILD_LIB_JKQTMATHTEXT, JKQtPlotter_BUILD_LIB_JKQTMATH</li>
<li>NEW add JKQTPExpected datatype</li>
<li>NEW add jkqtp_roundToDigits()</li>
<li>NEW add JKQTPExpected datatype, jkqtp_roundToDigits(), generic RegularExpression functions (jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt() )</li>
<li>NEW CSS-parser JKQTPCSSParser in order to parse e.g. \c linear-gradient() specifications in jkqtp_String2QBrushStyleExt()</li>
</ul></li>
<li>JKQTPlotter:<ul>

View File

@ -28,6 +28,7 @@ isEmpty(JKQTP_COMMON_PRI_INCLUDED) {
$$PWD/jkqtcommon/jkqtphighrestimer.h \
$$PWD/jkqtcommon/jkqttools.h \
$$PWD/jkqtcommon/jkqtpicons.h \
$$PWD/jkqtcommon/jkqtpcsstools.h \
$$PWD/jkqtcommon/jkqtpcachingtools.h \
$$PWD/jkqtcommon/jkqtpconcurrencytools.h \
$$PWD/jkqtcommon/jkqtpexpected.h
@ -45,6 +46,7 @@ isEmpty(JKQTP_COMMON_PRI_INCLUDED) {
$$PWD/jkqtcommon/jkqtphighrestimer.cpp \
$$PWD/jkqtcommon/jkqttools.cpp \
$$PWD/jkqtcommon/jkqtpicons.cpp \
$$PWD/jkqtcommon/jkqtpcsstools.cpp \
$$PWD/jkqtcommon/jkqtpcachingtools.cpp \
$$PWD/jkqtcommon/jkqtpconcurrencytools.cpp

View File

@ -29,6 +29,7 @@ target_sources(${lib_name} PRIVATE
jkqtphighrestimer.cpp
jkqttools.cpp
jkqtpicons.cpp
jkqtpcsstools.cpp
)
# ... and add headers
target_sources(${lib_name} PUBLIC FILE_SET HEADERS TYPE HEADERS
@ -47,6 +48,7 @@ target_sources(${lib_name} PUBLIC FILE_SET HEADERS TYPE HEADERS
jkqtpgeometrytools.h
jkqtpconcurrencytools.h
jkqtpcachingtools.h
jkqtpcsstools.h
jkqtpexpected.h
)

View File

@ -0,0 +1,417 @@
/*
Copyright (c) 2008-2024 Jan W. Krieger (<jan@jkrieger.de>)
This software is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License (LGPL) as published by
the Free Software Foundation, either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License (LGPL) for more details.
You should have received a copy of the GNU Lesser General Public License (LGPL)
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "jkqtcommon/jkqtpcsstools.h"
#include "jkqtcommon/jkqtpstringtools.h"
#include <exception>
#include <QMetaEnum>
JKQTPCSSParser::RawErrorTag_t JKQTPCSSParser::RawErrorTag={};
JKQTPCSSParser::Token JKQTPCSSParser::peekNextToken(int *endPos)
{
// on "RETURN" fromm this function: reset to current status and store the new pos in oldPos (if !=nullptr)
auto _final=JKQTPFinally(std::bind([&](JKQTPCSSParser* p, const Token oldToken, const int oldPos){
if (endPos) *endPos=p->pos;
p->CurrentToken=oldToken;
p->pos=oldPos;
}, this, CurrentToken, pos));
QChar ch('\0');
while(getCh(ch) && ch.isSpace()) {
}
static QString hexDigits="0123456789abcdefABCDEF";
if (ch==QChar('\0')) return Token(Token::END);
else if (ch==QChar(',')) return Token(Token::COMMA);
else if (ch==QChar('(')) return Token(Token::LBRACKET);
else if (ch==QChar(')')) return Token(Token::RBRACKET);
else if (ch==QChar('/')) return Token(Token::SLASH);
else if (ch==QChar('#')) {
QString hex;
while (peekCh(ch) && hexDigits.contains(ch)) {
getCh(ch);
hex+=ch;
}
if (hex.size()==0) throw std::runtime_error(QString("did not find HEX digits after a HASH '#' at pos.%1!").arg(pos).toStdString());
return Token(hex,Token::HEXSTRING);
} else if (ch.isDigit() || ch=='+' || ch=='-' || ch=='.') {
QString num=ch;
QString unit;
while (peekCh(ch) && (ch.isDigit() || ch=='.')) {
getCh(ch);
num+=ch;
}
if (ch.isLetter() || ch=='\xB0' || ch=='%') {
unit=ch;
getCh(ch);
while (peekCh(ch) && (ch.isLetter() || ch=='\xB0' || ch=='%')) {
getCh(ch);
unit+=ch;
}
}
return Token(num.toDouble(), unit);
} else if (ch.isLetter()) {
QString name=ch;
while (peekCh(ch) && (ch.isLetterOrNumber() ||ch=='-' || ch=='_'|| ch=='.')) {
getCh(ch);
name+=ch;
}
return Token(name);
} else {
throw std::runtime_error(QString("found unexpected character '%1' at pos.%2").arg(ch).arg(pos).toStdString());
}
return Token::END;
}
JKQTPCSSParser::Token JKQTPCSSParser::getToken()
{
int newpos=pos;
const auto newT=peekNextToken(&newpos);
pos=newpos;
return CurrentToken=newT;
}
JKQTPExpected<JKQTPCSSParser::NumberWithUnit, JKQTPCSSParser::GeneralError> JKQTPCSSParser::parseNumberWithUnit(bool get)
{
NumberWithUnit num = NumberWithUnit(0.0,"");
if (get) getToken();
if (CurrentToken.is(Token::TokenType::NUMBER)) {
num=NumberWithUnit(CurrentToken.NumberValue, CurrentToken.StringValue);
} else {
return {JKQTPUnexpected, UnexpectedTokenError(Token::NUMBER, CurrentToken, pos) };
}
return { num };
}
JKQTPExpected<QColor, JKQTPCSSParser::GeneralError> JKQTPCSSParser::parseColor(bool get)
{
QColor col=Qt::black;
if (get) getToken();
if (CurrentToken.is(Token::TokenType::HEXSTRING)) {
const QString hex=CurrentToken.StringValue;
if (hex.size()==3) {
const int r=QString(hex[0]).toInt(nullptr, 16);
const int g=QString(hex[1]).toInt(nullptr, 16);
const int b=QString(hex[2]).toInt(nullptr, 16);
col=QColor::fromRgb(r*16+r,g*16+g,b*16+b);
} else if (hex.size()==4) {
const int r=QString(hex[0]).toInt(nullptr, 16);
const int g=QString(hex[1]).toInt(nullptr, 16);
const int b=QString(hex[2]).toInt(nullptr, 16);
const int a=QString(hex[3]).toInt(nullptr, 16);
col=QColor::fromRgb(r*16+r,g*16+g,b*16+b,a*16+a);
} else if (hex.size()==6) {
col=QColor::fromRgb(hex.mid(0,2).toInt(nullptr, 16),hex.mid(2,2).toInt(nullptr, 16),hex.mid(4,2).toInt(nullptr, 16));
} else if (hex.size()==8) {
col=QColor::fromRgb(hex.mid(0,2).toInt(nullptr, 16),hex.mid(2,2).toInt(nullptr, 16),hex.mid(4,2).toInt(nullptr, 16),hex.mid(6,2).toInt(nullptr, 16));
} else {
return {JKQTPUnexpected, UnconvertobleError("#"+hex, "HEX-RGB value", pos) };
}
} else if (CurrentToken.is(Token::TokenType::NAME)) {
static QMap<QString,QPair<int,int>> rgbFuncs={
{"rgb", QPair<int,int>(3,4)},
{"rgba",QPair<int,int>(4,4)},
{"hsl",QPair<int,int>(3,4)},
{"hsv",QPair<int,int>(3,4)},
{"gray",QPair<int,int>(1,2)},
{"grey",QPair<int,int>(1,2)},
{"red",QPair<int,int>(1,2)},
{"green",QPair<int,int>(1,2)},
{"blue",QPair<int,int>(1,2)}
};
const QString func=CurrentToken.getNormString();
if (rgbFuncs.contains(func)) {
int nextPos=pos;
const auto nextT=peekNextToken(&nextPos);
if (nextT.is(Token::END)) {
getToken();
return jkqtp_lookupQColorName(func);
} else if (!nextT.is(Token::LBRACKET)) {
bool found=false;
col=jkqtp_lookupQColorName(func, false, &found);
if (!found) {
getToken();
return {JKQTPUnexpected, UnexpectedTokenError(func, Token::LBRACKET, CurrentToken, pos) };
} else return { col };
}
getToken();
const int minParams=rgbFuncs[func].first;
const int maxParams=rgbFuncs[func].second;
QList<double> params;
getToken();
while (CurrentToken.is(Token::NUMBER)) {
const auto num=parseNumberWithUnit(false);
if (!num.has_value()) return {JKQTPUnexpected, UnexpectedTermError(func, "number with unit", CurrentToken, pos) };
if (params.size()==0 && (func=="hsl" || func=="hsv")) params.append(num.value().normHueNumber());
else {
if ((minParams!=maxParams && params.size()==maxParams-1) || minParams==4) params.append(jkqtp_bounded(0.0,num.value().normNumber(),1.0));
else params.append(num.value().normRGBNumber());
}
getToken();
if (CurrentToken.is(Token::COMMA) || CurrentToken.is(Token::SLASH)) getToken();
}
if (!CurrentToken.is(Token::RBRACKET) && !CurrentToken.is(Token::END)) return {JKQTPUnexpected, UnexpectedTokenError(func, Token::RBRACKET, CurrentToken, pos) };
if (params.size()>=minParams && params.size()<=maxParams) {
if (func=="rgb" || func=="rgba") {
col=QColor::fromRgbF(params[0],params[1],params[2],params.value(3,1));
} else if (func=="hsl") {
col=QColor::fromHslF(params[0],params[1],params[2],params.value(3,1));
} else if (func=="hsv") {
col=QColor::fromHsvF(params[0],params[1],params[2],params.value(3,1));
} else if (func=="gray" || func=="grey") {
col=QColor::fromRgbF(params[0],params[0],params[0],params.value(1,1));
} else if (func=="red") {
col=QColor::fromRgbF(params[0],0.0,0.0,params.value(1,1));
} else if (func=="green") {
col=QColor::fromRgbF(0.0,params[0],0.0,params.value(1,1));
} else if (func=="blue") {
col=QColor::fromRgbF(0.0,0.0,params[0],params.value(1,1));
}
} else return {JKQTPUnexpected, WrongNumberOfArgumentError(func, params.size(), minParams, maxParams, pos) };
} else {
col=jkqtp_lookupQColorName(CurrentToken.StringValue);
}
} else {
return {JKQTPUnexpected, UnexpectedTokenError(Token::NUMBER, CurrentToken, pos) };
}
return { col };
}
JKQTPExpected<QGradient, JKQTPCSSParser::GeneralError> JKQTPCSSParser::parseGradient(bool get)
{
QGradient grad=QGradient();
if (get) getToken();
static QMap<QString,QGradient::Preset> s_GradientPresets = []() {
QMap<QString,QGradient::Preset> m;
for (int i=1; i<QGradient::Preset::NumPresets; i++) {
const QString id=QString(QMetaEnum::fromType<QGradient::Preset>().valueToKey(static_cast<QGradient::Preset>(i))).toLower().trimmed().simplified();
if (id.size()>0) m[id]=static_cast<QGradient::Preset>(i);
}
return m;
}();
const QString func=CurrentToken.StringValue.trimmed().simplified().toLower();
if (CurrentToken.is(Token::TokenType::NAME) && func=="linear-gradient") {
QGradientStops colorStops;
double alphaDeg=180; // 0 = bottom->top angle like clock-pointer
// 90 = left->right
// 180 = top->bottom
// 270 = right->left
getToken();
if (!CurrentToken.is(Token::LBRACKET)) return {JKQTPUnexpected, UnexpectedTokenError(func, Token::LBRACKET, CurrentToken, pos) };
getToken();
if (CurrentToken.isNormString("to")) {
QStringList capDir;
static QSet<QString> allowedDirs={"left","right","top","bottom"};
while (getToken().is(Token::NAME)) {
const QString ns=CurrentToken.getNormString();
if (allowedDirs.contains(ns)) {
capDir.append(ns);
} else {
return {JKQTPUnexpected, UnexpectedTermError(func, "direction specifier [left|right|top|bottom] after 'to'", CurrentToken, pos) };
}
}
if (capDir.size()==1 || (capDir.size()==2 && capDir[1].isEmpty())) {
if (capDir[0]=="left") alphaDeg=270;
else if (capDir[0]=="right") alphaDeg=90;
else if (capDir[0]=="top") alphaDeg=0;
else if (capDir[0]=="bottom") alphaDeg=180;
} else if (capDir.size()==2) {
if (capDir[0]=="top"||capDir[0]=="bottom") qSwap(capDir[0],capDir[1]);
if (capDir[0]=="left" && capDir[1]=="top") alphaDeg=270+45;
else if (capDir[0]=="left" && capDir[1]=="bottom") alphaDeg=270-45;
else if (capDir[0]=="right" && capDir[1]=="top") alphaDeg=90-45;
else if (capDir[0]=="right" && capDir[1]=="bottom") alphaDeg=90+45;
} else {
return {JKQTPUnexpected, GeneralError(func, "at most two directions allowed after 'to', but found "+QString::number(capDir.size()), pos) };
}
if (!CurrentToken.is(Token::COMMA)) return {JKQTPUnexpected, UnexpectedTokenError(func, Token::COMMA, CurrentToken, pos) };
//getToken();
} if (CurrentToken.is(Token::NUMBER)) {
auto angle=parseNumberWithUnit(false);
alphaDeg=angle.value().normNumber();
getToken();
}
// alphaDeg 0 90 180 270
// sin 0 1 0 -1
// cos 1 0 -1 0
// up right down left
const double dx=jkqtp_roundToDigits(0.5*sin(alphaDeg/180.0*M_PI),10);
const double dy=jkqtp_roundToDigits(-0.5*cos(alphaDeg/180.0*M_PI),10);
QLinearGradient lgrad(0.5-dx,0.5-dy,0.5+dx,0.5+dy);
lgrad.setCoordinateMode(QGradient::ObjectBoundingMode);
bool done=false;
while (!done) {
QGradientStop stop;
const auto col=parseColor(true);
if (!col.has_value()) return {JKQTPUnexpected, UnexpectedTermError(func, "color specifier", CurrentToken, pos) };
stop.second=col.value();
stop.first=-1;
getToken();
if (CurrentToken.is(Token::NUMBER)) {
if (CurrentToken.StringValue!="%") return {JKQTPUnexpected, UnexpectedTermError(func, "percentage value", CurrentToken, pos) };
stop.first=CurrentToken.NumberValue/100.0;
getToken();
}
colorStops<<stop;
if (CurrentToken.is(Token::RBRACKET) || CurrentToken.is(Token::END)) {
done=true;
}
}
if (colorStops.size()>0) {
// normalize colorStop positions
if (colorStops.first().first<0) colorStops.first().first=0;
if (colorStops.size()>1) {
if (colorStops.last().first<0) colorStops.last().first=1.0;
}
int i=1;
while (i<colorStops.size()) {
if(colorStops[i].first<0) {
const double lastStop=colorStops[i-1].first;
const int lastI=i-1;
while (colorStops[i].first<0) i++;
const double endStop=colorStops[i].first;
const int cnt=i-lastI-1;
const double delta=(endStop-lastStop)/static_cast<double>(cnt+1);
for (int j=lastI+1; j<i; j++) {
colorStops[j].first=lastStop+static_cast<double>(j-lastI)*delta;
}
}
i++;
}
lgrad.setStops(colorStops);
grad=lgrad;
}
} else if (CurrentToken.isNormStringAnyOf(s_GradientPresets.keys())) {
grad=QGradient(s_GradientPresets[CurrentToken.getNormString()]);
grad.setCoordinateMode(QGradient::ObjectBoundingMode);
} else {
return {JKQTPUnexpected, UnexpectedTermError("supported gradient-function [linear-gradient|] or predefined gradient name", CurrentToken, pos) };
}
return { grad };
}
JKQTPCSSParser::JKQTPCSSParser(const QString &text_):
CurrentToken(Token::TokenType::END), text(text_), pos(0)
{
}
QString JKQTPCSSParser::Token::toString(TokenType type)
{
switch(type) {
case END: return "END";
case NAME: return "NAME";
case NUMBER: return "NUMBER";
case HEXSTRING: return "HEXSTRING";
case LBRACKET: return "LBRACKET";
case RBRACKET: return "RBRACKET";
case COMMA: return "COMMA";
case SLASH: return "SLASH";
}
return "???";
}
JKQTPCSSParser::Token::Token():
type(TokenType::END), StringValue(""), NumberValue(0.0)
{
}
JKQTPCSSParser::Token::Token(TokenType type):
type(type), StringValue(""), NumberValue(0.0)
{
switch(type) {
case LBRACKET: StringValue="("; break;
case RBRACKET: StringValue=")"; break;
case COMMA: StringValue=","; break;
default: break;
}
}
JKQTPCSSParser::Token::Token(double num, const QString &unit_):
NumberValue(num), StringValue(unit_), type(NUMBER)
{
}
JKQTPCSSParser::Token::Token(const QString &str, TokenType type_):
StringValue(str), type(type_)
{
if (type_==NUMBER) NumberValue=str.toDouble();
if (type_==HEXSTRING) NumberValue=str.toInt(nullptr,16);
}
QString JKQTPCSSParser::Token::toString() const
{
QString s=toString(type);
if (type==NUMBER) s+= " ["+QString::number(NumberValue, 'f')+"]";
else if (StringValue.size()>0) s+= " ["+StringValue+"]";
return s;
}
JKQTPCSSParser::NumberWithUnit JKQTPCSSParser::readNumberWithUnit(const QString &prog)
{
JKQTPCSSParser parser(prog);
const auto res= parser.parseNumberWithUnit(true);
if (res.has_value()) return res.value();
else throw std::runtime_error(res.error().error.toStdString());
}
QColor JKQTPCSSParser::readColor(const QString &prog)
{
JKQTPCSSParser parser(prog);
const auto res= parser.parseColor(true);
if (res.has_value()) return res.value();
else throw std::runtime_error(res.error().error.toStdString());
}
QGradient JKQTPCSSParser::readGradient(const QString &prog)
{
JKQTPCSSParser parser(prog);
const auto res= parser.parseGradient(true);
if (res.has_value()) return res.value();
else throw std::runtime_error(res.error().error.toStdString());
}

View File

@ -0,0 +1,299 @@
/*
Copyright (c) 2008-2024 Jan W. Krieger (<jan@jkrieger.de>)
This software is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef JKQTPCSSTOOLS_H_INCLUDED
#define JKQTPCSSTOOLS_H_INCLUDED
#include "jkqtcommon/jkqtcommon_imexport.h"
#include <QString>
#include <QColor>
#include <QGradient>
#include <QStack>
#include "jkqtcommon/jkqtpmathtools.h"
#include "jkqtcommon/jkqtpexpected.h"
/*! \brief A simple parser for certain CSS subsets
\ingroup jkqtptools_math_parser
\section jkqtpcss_ebnf EBNF definition of the parsed expressions
<pre>
<lingradient> -> <b>linear-gradient(</b> <angle>? <b>,</b> <gradientstops> <b>)</b>
<angle> -> <numberwithunit>
| <b>to</b> (<b>left</b>|<b>right</b>|<b>bottom</b>|<b>top</b>) (<b>left</b>|<b>right</b>|<b>bottom</b>|<b>top</b>)?
<numberwithunit> -> <number> [<b>A</b>-<b>Z</b><b>a</b>-<b>z</b><b>%</b>]*
<gradientstops> -> <gradientstop> | <gradientstop> <b>,</b> <gradientstops>
<gradientstop> -> <color> (<number><b>%</b>)?
<color> -> NAME
| <b>#</b><hexnumber>
| (<b>rgb</b> | <b>rgba</a> | <b>hsl</a> | <b>hsv</a> | <b>gray</a> | <b>grey</a> | <b>red</a> | <b>green</a> | <b>blue</a>) <b>(</b> (<numberwithunit> [<b>,/</b>]? ){1-4} <b>)</b>
<number> -> floating-point-number, i.e. "[+-]?\d+\.?\d*"
<hexnumber> -> RGB | RRGGBB | RGBA | RRGGBBAA
</pre>
*/
class JKQTCOMMON_LIB_EXPORT JKQTPCSSParser
{
public:
/** \brief encodes a number with its unit, e.g. \c 100% or \c 45deg or ... */
struct NumberWithUnit {
inline NumberWithUnit(double val=0.0, const QString& unit_=QString()): unit(unit_), number(val) {};
/** \bref unit as string */
QString unit;
/** \brief the number value ittself */
double number;
/** \brief partly evaluates the unit and transforms the number accordingly.
*
* This will do these transformations:
* - unit == \c % --> returns number/100.0;
* - unit == angle unit : returns number in degrees:
* - unit == \c rad --> returns number/M_PI*180.0
* - unit == \c turn --> returns number*360.0
* - unit == \c grad --> returns number/400.0*360.0
* .
* .
*/
inline double normNumber() const {
const QString nu=normUnit();
if (nu=="%") return number/100.0;
if (nu=="rad") return number/JKQTPSTATISTICS_PI*180.0;
if (nu=="turn") return number*360.0;
if (nu=="grad") return number/400.0*360.0;
return number;
}
/** \brief returns number as a normalized number for RGB, output is in the range 0..1
*
* This interprets %-values and all other values as from the range 0..255 and noralizes both to 0..1
*/
inline double normRGBNumber() const {
const QString nu=normUnit();
if (nu=="%") return jkqtp_bounded(0.0,number/100.0, 1.0);
return jkqtp_bounded(0.0,number/255.0, 1.0);
}
/** \brief returns number as a normalized number for Hue, output is in the range 0..1
*
* This interprets %-values and all other values as from the range 0..360 and noralizes both to 0..1
*/
inline double normHueNumber() const {
const QString nu=normUnit();
if (nu=="%") return jkqtp_bounded(0.0,number/100.0, 1.0);
if (nu=="rad") return fmod(number/(JKQTPSTATISTICS_PI*2.0),1.0);
if (nu=="turn") return fmod(number,1.0);
if (nu=="grad") return fmod(number/400.0,1.0);
return fmod(number/360.0, 1.0);
}
/** \brief returns a normalized version of the unit, i.e. all lower-case, timmed and simplified */
inline QString normUnit() const { return unit.simplified().trimmed().toLower(); }
inline bool operator==(const NumberWithUnit& other) const {
return number==other.number && unit==other.unit;
}
};
protected:
/** @name Tokenizer */
/**@{*/
struct Token {
/** \brief the possible Token that can be recognized by the tokenizer in JKQTPCSSParser::getToken() */
enum TokenType {
END, /*!< \brief end token */
NAME, /*!< \brief a name (consisting of characters) of a variable or function */
NUMBER, /*!< \brief a number, possibly with a unit(string) */
HEXSTRING, /*!< \brief a string in HEX notation (i.e. 0-9a-fA-F) */
LBRACKET, /*!< \brief left brackets '(' */
RBRACKET, /*!< \brief right brackets ')' */
COMMA, /*!< \brief a comma ',' */
SLASH, /*!< \brief a slash '/' */
};
static QString toString(TokenType type);
Token();
Token(TokenType type);
Token(double num, const QString& unit_=QString());
Token(const QString& str, TokenType type=NAME);
TokenType type;
/** \brief the string value of the current token (when applicable) during the parsing step */
QString StringValue;
/** \brief the string value of the current token (when applicable) during the parsing step */
double NumberValue;
/** \brief checks whether the current token is of the given type (does not compare any other field) */
inline bool is(TokenType othertype) const {
return (type==othertype);
}
/** \brief checks whether the current token is of type TokenType::NAME and the String value equals \a name (case-insensitive commparison, trimmer, simplified) */
inline bool isNormString(const QString& name) const {
return (type==TokenType::NAME) && (getNormString()==name.toLower().simplified().trimmed());
}
/** \brief checks whether the current token is of type TokenType::NAME and the String value equals any of the entries in \a names (case-insensitive commparison, trimmer, simplified) */
inline bool isNormStringAnyOf(const QStringList& names) const {
if (type==TokenType::NAME) {
const QString ns=getNormString();
for (const auto&name: names) {
if (ns==name.toLower().simplified().trimmed()) return true;
}
}
return false;
}
/** \brief normlizes the StringValue (i.e. lower-case, trimmer, simmplified) */
inline QString getNormString() const {
return StringValue.toLower().simplified().trimmed();
}
/** \brief converts the TOken information to a string */
QString toString() const;
};
/** \brief Tokenizer: look at the next token from the input, but don't set it as CurrentToken and don't move the read pointer pos */
Token peekNextToken(int* endPos=nullptr);
/** \brief Tokenizer: extract the next token from the input */
Token getToken();
/** \brief the current token while parsing a string */
Token CurrentToken;
/** \brief this stream is used to read in the program. An object is created and assigned
* (and destroyed) by the parse()-function */
QString text;
/** \brief current reading position in text */
int pos;
/** \brief indicates whether pos points to the end of text */
inline bool textAtEnd() const {
return pos<0 || pos>=text.size()-1;
}
inline bool getCh(QChar& ch) {
if (pos<0 || pos>=text.size()) return false;
ch=text[pos];
pos++;
return true;
}
inline bool peekCh(QChar& ch) {
if (pos<0 || pos>=text.size()) return false;
ch=text[pos];
return true;
}
inline void putBackCh() {
pos--;
}
/**@}*/
/** @name Error Handling */
/**@{*/
struct RawErrorTag_t {};
static RawErrorTag_t RawErrorTag;
struct GeneralError {
inline GeneralError(const QString& err=QString("unspecified error"), int p=-1): error(QString("%1 at pos.%2").arg(err).arg(p)), pos(p) {}
inline GeneralError(RawErrorTag_t, const QString& err, int p=-1): error(err), pos(p) { }
inline GeneralError(const QString& context, const QString& err, int p=-1): error(QString("%1 in context %3 at pos.%2").arg(err).arg(p).arg(context)), pos(p) {}
QString error;
int pos;
};
/** \brief Exception for unexpected Token */
struct UnexpectedTokenError: public GeneralError {
inline UnexpectedTokenError(Token::TokenType expectedToken, Token::TokenType foundToken, int pos):
GeneralError(RawErrorTag, QString("unexpected token at pos.%1 (found: %2, expected: %3)").arg(pos).arg(Token::toString(foundToken)).arg(Token::toString(expectedToken)), pos)
{}
inline UnexpectedTokenError(Token::TokenType expectedToken, const Token& foundToken, int pos):
GeneralError(RawErrorTag, QString("unexpected token at pos.%1 (found: %2, expected: %3)").arg(pos).arg(foundToken.toString()).arg(Token::toString(expectedToken)), pos)
{}
inline UnexpectedTokenError(const QString& context, Token::TokenType expectedToken, const Token& foundToken, int pos):
GeneralError(RawErrorTag, QString("unexpected token at pos.%1 in context %4 (found: %2, expected: %3)").arg(pos).arg(foundToken.toString()).arg(Token::toString(expectedToken)).arg(context), pos)
{}
};
/** \brief Exception for unexpected Term */
struct UnexpectedTermError: public GeneralError {
inline UnexpectedTermError(const QString& expectedToken, const Token& foundToken, int pos):
GeneralError(RawErrorTag, QString("unexpected term at pos.%1 (found: %2, expected: %3)").arg(pos).arg(foundToken.toString()).arg(expectedToken), pos)
{}
inline UnexpectedTermError(const QString& context, const QString& expectedToken, const Token& foundToken, int pos):
GeneralError(RawErrorTag, QString("unexpected term at pos.%1 in context %4 (found: %2, expected: %3)").arg(pos).arg(foundToken.toString()).arg(expectedToken).arg(context), pos)
{}
};
/** \brief Exception for wrong number of function arguments */
struct WrongNumberOfArgumentError: public GeneralError {
inline WrongNumberOfArgumentError(const QString& func, int numArgs, int minArgs, int maxArgs, int pos):
GeneralError(RawErrorTag, QString("wrong number of function arguments for %2() found at pos.%1 (found: %3, expected: %4...%5)").arg(pos).arg(func).arg(numArgs).arg(minArgs).arg(maxArgs), pos)
{}
};
/** \brief Exception when a string cannot be converted properly */
struct UnconvertobleError: public GeneralError {
inline UnconvertobleError(const QString& str, const QString& target, int pos):
GeneralError(RawErrorTag, QString("Could not convert '%2' to %3 at pos.%1").arg(pos).arg(str).arg(target), pos)
{}
};
/**@}*/
/** @name Parser */
/**@{*/
/** \brief parses a number with unit */
JKQTPExpected<JKQTPCSSParser::NumberWithUnit, JKQTPCSSParser::GeneralError> parseNumberWithUnit(bool get);
/** \brief parses a color definition <color> */
JKQTPExpected<QColor, JKQTPCSSParser::GeneralError> parseColor(bool get);
/** \brief parses a color definition <gradient> */
JKQTPExpected<QGradient, JKQTPCSSParser::GeneralError> parseGradient(bool get);
/**@}*/
/** \brief class constructor
*
* \note This also registers all standatd functions and constants by calling addStandardFunctions() and addStandardVariables()
*/
JKQTPCSSParser(const QString& text);
public:
/** \brief parses the given expression*/
static NumberWithUnit readNumberWithUnit(const QString& prog);
/** \brief parses the given expression, which should represent a color (incl. color-functions like \c rgb() ) */
static QColor readColor(const QString& prog);
/** \brief parses the given expression, which should represent a QGradient */
static QGradient readGradient(const QString& prog);
};
#endif // JKQTPCSSTOOLS_H_INCLUDED

View File

@ -20,6 +20,7 @@
#include "jkqtcommon/jkqtpstringtools.h"
#include "jkqtcommon/jkqtpmathtools.h"
#include "jkqtcommon/jkqtpcsstools.h"
#include <QDateTime>
#include <cmath>
#include <QDebug>
@ -34,6 +35,11 @@
#include <ctype.h>
#include <sstream>
#include <locale>
#include <QCache>
#include <QSharedPointer>
#include <QMetaEnum>
#include <QGradient>
#include <QGradientStops>
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
#include<QRegularExpression>
#include<QRegularExpressionMatch>
@ -330,26 +336,73 @@ QString jkqtp_QBrushStyle2String(Qt::BrushStyle style) {
}
}
namespace {
static QMap<QString,Qt::BrushStyle> s_String2QBrushStyleMap = []() {
QMap<QString,Qt::BrushStyle> m;
m["none"]=Qt::NoBrush;
m["d1"]=Qt::Dense1Pattern;
m["d2"]=Qt::Dense2Pattern;
m["d3"]=Qt::Dense3Pattern;
m["d4"]=Qt::Dense4Pattern;
m["d5"]=Qt::Dense5Pattern;
m["d6"]=Qt::Dense6Pattern;
m["d7"]=Qt::Dense7Pattern;
m["hor"]=Qt::HorPattern;
m["ver"]=Qt::VerPattern;
m["cross"]=Qt::CrossPattern;
m["bdiag"]=Qt::BDiagPattern;
m["vdiag"]=Qt::FDiagPattern;
m["diagcross"]=Qt::DiagCrossPattern;
return m;
}();
static QMap<QString,QGradient::Preset> s_GradientPresets = []() {
QMap<QString,QGradient::Preset> m;
for (int i=1; i<QGradient::Preset::NumPresets; i++) {
const QString id=QString(QMetaEnum::fromType<QGradient::Preset>().valueToKey(static_cast<QGradient::Preset>(i))).toLower().trimmed().simplified();
if (id.size()>0) m[id]=static_cast<QGradient::Preset>(i);
}
return m;
}();
}
Qt::BrushStyle jkqtp_String2QBrushStyle(const QString& style) {
QString s=style.toLower().trimmed();
if (s=="none") return Qt::NoBrush;
if (s=="d1") return Qt::Dense1Pattern;
if (s=="d2") return Qt::Dense2Pattern;
if (s=="d3") return Qt::Dense3Pattern;
if (s=="d4") return Qt::Dense4Pattern;
if (s=="d5") return Qt::Dense5Pattern;
if (s=="d6") return Qt::Dense6Pattern;
if (s=="d7") return Qt::Dense7Pattern;
if (s=="hor") return Qt::HorPattern;
if (s=="ver") return Qt::VerPattern;
if (s=="cross") return Qt::CrossPattern;
if (s=="bdiag") return Qt::BDiagPattern;
if (s=="vdiag") return Qt::FDiagPattern;
if (s=="diagcross") return Qt::DiagCrossPattern;
/*if (s=="lingrad") return Qt::LinearGradientPattern;
if (s=="radgrad") return Qt::RadialGradientPattern;
if (s=="congrad") return Qt::ConicalGradientPattern;*/
return Qt::SolidPattern;
return s_String2QBrushStyleMap.value(s, Qt::SolidPattern);
}
Qt::BrushStyle jkqtp_String2QBrushStyleExt(const QString &style, QColor *color, QGradient *gradient, QPixmap *image, double* rotationAngleDeg)
{
const QString s=style.toLower().trimmed().simplified();
QStringList caps;
if (s.startsWith("linear-gradient(")) {
try {
const auto grad=JKQTPCSSParser::readGradient(s);
if (gradient) *gradient=grad;
return Qt::LinearGradientPattern;
} catch(std::exception& E) {
qWarning()<<"error converting '"<<style<<"' into a QGradient: "<<E.what();
return Qt::SolidPattern;
}
} else if (s_GradientPresets.contains(s)) {
QGradient g(s_GradientPresets[s]);
g.setCoordinateMode(QGradient::ObjectBoundingMode);
if (gradient) *gradient=g;
if (g.type()==QGradient::Type::RadialGradient) return Qt::RadialGradientPattern;
else if (g.type()==QGradient::Type::ConicalGradient) return Qt::ConicalGradientPattern;
else return Qt::LinearGradientPattern;
} else if (jkqtp_rxExactlyMatches(s, "\\s*image\\s*\\(\\s*[\\\"\\\']?(.*)[\\\"\\\']?\\s*\\)\\s*", &caps)) {
if (image) *image=QPixmap(caps[1]);
return Qt::TexturePattern;
} else {
// now we parse:
// pattern ::= none | d1 | d2 | ... | cross | bdiag | ...
return jkqtp_String2QBrushStyle(s);
}
}
@ -576,36 +629,37 @@ QString jkqtp_QColor2String(QColor color, bool useSpecialTransparencySyntax) {
QColor jkqtp_lookupQColorName(const QString &color, bool namesOnly, bool *nameFound) {
if (nameFound) *nameFound=true;
const QString col=color.toLower().trimmed();
if (col=="window") return QGuiApplication::palette().color(QPalette::Window);
if (col=="windowtext") return QGuiApplication::palette().color(QPalette::WindowText);
if (col=="button") return QGuiApplication::palette().color(QPalette::Button);
if (col=="light") return QGuiApplication::palette().color(QPalette::Light);
if (col=="midlight") return QGuiApplication::palette().color(QPalette::Midlight);
if (col=="dark") return QGuiApplication::palette().color(QPalette::Dark);
if (col=="mid") return QGuiApplication::palette().color(QPalette::Mid);
if (col=="text") return QGuiApplication::palette().color(QPalette::Text);
if (col=="brightttext") return QGuiApplication::palette().color(QPalette::BrightText);
if (col=="base") return QGuiApplication::palette().color(QPalette::Base);
if (col=="shadow") return QGuiApplication::palette().color(QPalette::Shadow);
if (col=="highlight") return QGuiApplication::palette().color(QPalette::Highlight);
if (col=="highlightedtext") return QGuiApplication::palette().color(QPalette::HighlightedText);
if (col=="link") return QGuiApplication::palette().color(QPalette::Link);
if (col=="linkvisited") return QGuiApplication::palette().color(QPalette::LinkVisited);
if (col=="alternatebase") return QGuiApplication::palette().color(QPalette::AlternateBase);
if (col=="norole") return QGuiApplication::palette().color(QPalette::NoRole);
if (col=="tooltipbase") return QGuiApplication::palette().color(QPalette::ToolTipBase);
if (col=="tooltiptext") return QGuiApplication::palette().color(QPalette::ToolTipText);
const QString col=color.toLower().trimmed();
if (col=="transparent") return QColor::fromRgb(0,0,0,0);
if (col=="window") return QGuiApplication::palette().color(QPalette::Window);
if (col=="windowtext") return QGuiApplication::palette().color(QPalette::WindowText);
if (col=="button") return QGuiApplication::palette().color(QPalette::Button);
if (col=="light") return QGuiApplication::palette().color(QPalette::Light);
if (col=="midlight") return QGuiApplication::palette().color(QPalette::Midlight);
if (col=="dark") return QGuiApplication::palette().color(QPalette::Dark);
if (col=="mid") return QGuiApplication::palette().color(QPalette::Mid);
if (col=="text") return QGuiApplication::palette().color(QPalette::Text);
if (col=="brightttext") return QGuiApplication::palette().color(QPalette::BrightText);
if (col=="base") return QGuiApplication::palette().color(QPalette::Base);
if (col=="shadow") return QGuiApplication::palette().color(QPalette::Shadow);
if (col=="highlight") return QGuiApplication::palette().color(QPalette::Highlight);
if (col=="highlightedtext") return QGuiApplication::palette().color(QPalette::HighlightedText);
if (col=="link") return QGuiApplication::palette().color(QPalette::Link);
if (col=="linkvisited") return QGuiApplication::palette().color(QPalette::LinkVisited);
if (col=="alternatebase") return QGuiApplication::palette().color(QPalette::AlternateBase);
if (col=="norole") return QGuiApplication::palette().color(QPalette::NoRole);
if (col=="tooltipbase") return QGuiApplication::palette().color(QPalette::ToolTipBase);
if (col=="tooltiptext") return QGuiApplication::palette().color(QPalette::ToolTipText);
#if (QT_VERSION>=QT_VERSION_CHECK(5, 12, 0))
if (col=="placeholdertext") return QGuiApplication::palette().color(QPalette::PlaceholderText);
if (col=="placeholdertext") return QGuiApplication::palette().color(QPalette::PlaceholderText);
#endif
for (int i=0; i<rgbTblSize; i++) {
for (int i=0; i<rgbTblSize; i++) {
if (col==rgbTbl[i].name) return QColor(rgbTbl[i].value);
}
if (!namesOnly) return QColor(color);
if (nameFound) *nameFound=false;
return Qt::black;
}
if (!namesOnly) return QColor(color);
if (nameFound) *nameFound=false;
return Qt::black;
}
QColor jkqtp_String2QColor(QString color)
@ -616,8 +670,7 @@ QColor jkqtp_String2QColor(QString color)
if (color.startsWith('"')||color.startsWith('\'')) color=color.mid(1);
if (color.endsWith('"')||color.endsWith('\'')) color=color.left(color.size()-1);
if (color.isEmpty()) return Qt::black;
// fish out all #RGB #RRGGBB, ... sequences, withoud added alpha/transparency-value after comma
if (color.startsWith("#") && !color.contains(",")) return QColor(color);
// check whether we have a names color
{
bool nameFound=false;
@ -652,91 +705,37 @@ QColor jkqtp_String2QColor(QString color)
// now we check for diverse special syntaxes
// P: "color,NN%" NN=TRANSPARENCY in percent
// AP: "color,aNN\%" NN=ALPHA in percent
// NP: "color,[a]NN\%" NN=ALPHA 0..255
// NP: "color,[a]NN" NN=ALPHA 0..255
// Frac: "grey25"
// ColMod: "rgb(R,G,B)", "hsl(H,S,V)" ... (CSS-Syntax)
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
static QRegularExpression rxP("(.+)\\s*,\\s*t?\\s*(\\d+\\.?\\d+)\\%");
static QRegularExpression rxP("([^,\\(\\)]+)\\s*,\\s*t?\\s*(\\d+\\.?\\d+)\\%");
static QRegularExpression rxFrac("([a-zA-Z]{3,})(\\d{1,3})\\%?");
static QRegularExpression rxColMod("\\s*(rgb|hsl|hsv|rgba|gray|grey|red|green|blue)\\(\\s*(\\d+\\.?\\d*)(%|deg)?(?:\\s+|\\s*,\\s*|\\s\\/\\s)?(\\d+\\.?\\d*)?(%)?(?:\\s+|\\s*,\\s*|\\s\\/\\s)?(\\d+\\.?\\d*)?(%)?(?:\\s+|\\s*,\\s*|\\s*\\/\\s*)?(\\d+\\.?\\d*)?(%)?\\s*\\)\\s*");
static QRegularExpression rxAP("(.+)\\s*,\\s*a\\s*(\\d+\\.?\\d+)\\%");
static QRegularExpression rxNP("(.+)\\s*,\\s*a?\\s*([\\d]+)");
static QRegularExpression rxAP("([^,\\(\\)]+)\\s*,\\s*a\\s*(\\d+\\.?\\d+)\\%");
static QRegularExpression rxNP("([^,\\(\\)]+)\\s*,\\s*a?\\s*([\\d]+)");
const auto mP=rxP.match(color);
const auto mColMod=rxColMod.match(color);
if (mColMod.hasMatch()) {
QColor col(Qt::black);
const QString name=mColMod.captured(1);
const int v1=valUnitToInt(mColMod.captured(2), mColMod.captured(3));
const int h1=valUnitToInt(mColMod.captured(2), mColMod.captured(3), INT_MAX);
const int v2=valUnitToInt(mColMod.captured(4), mColMod.captured(5));
const int a2=valUnitToAlphaInt(mColMod.captured(4), mColMod.captured(5));
const int v3=valUnitToInt(mColMod.captured(6), mColMod.captured(7));
const int a4=valUnitToAlphaInt(mColMod.captured(8), mColMod.captured(9));
if (name=="gray"||name=="grey") {
if (v2<0) col.setRgb(v1,v1,v1);
else col.setRgb(v1,v1,v1,a2);
return col;
}
else if (name=="red") {
col.setRed(v1);
if (v2>=0) col.setAlpha(a2);
return col;
}
else if (name=="green") {
col.setGreen(v1);
if (v2>=0) col.setAlpha(a2);
return col;
}
else if (name=="blue") {
col.setBlue(v1);
if (v2>=0) col.setAlpha(a2);
return col;
}
else if (name=="rgb") {
if (a4<0) col.setRgb(v1,v2,v3);
else col.setRgb(v1,v2,v3,a2);
return col;
}
else if (name=="rgba") {
col.setRgb(v1,v2,v3,a2);
return col;
}
else if (name=="hsl") {
if (a4<0) col.setHsl(h1,v2,v3);
else col.setHsl(h1,v2,v3,a2);
return col;
}
else if (name=="hsv") {
if (a4<0) col.setHsv(h1,v2,v3);
else col.setHsv(h1,v2,v3,a2);
return col;
} else {
qDebug()<<"unrecognized CSS-color:'"<<color<<"'";
}
}
if (mP.hasMatch()) {
QColor col=jkqtp_lookupQColorName(mP.captured(1));
if (mP.hasMatch() && mP.captured(0).size()==color.size()) {
QColor col=jkqtp_String2QColor(mP.captured(1));
const double a=QLocale::c().toDouble(mP.captured(2));
col.setAlphaF(1.0-a/100.0);
return col;
}
const auto mAP=rxAP.match(color);
if (mAP.hasMatch()) {
QColor col=jkqtp_lookupQColorName(mAP.captured(1));
if (mAP.hasMatch() && mAP.captured(0).size()==color.size()) {
QColor col=jkqtp_String2QColor(mAP.captured(1));
const double a=QLocale::c().toDouble(mAP.captured(2));
col.setAlphaF(a/100.0);
return col;
}
const auto mNP=rxNP.match(color);
if (mNP.hasMatch()) {
QColor col=jkqtp_lookupQColorName(mNP.captured(1));
if (mNP.hasMatch() && mNP.captured(0).size()==color.size()) {
QColor col=jkqtp_String2QColor(mNP.captured(1));
const double a=QLocale::c().toInt(mNP.captured(2));
col.setAlphaF(a/255.0);
return col;
}
const auto mFrac=rxFrac.match(color);
if (mFrac.hasMatch()) {
if (mFrac.hasMatch() && mFrac.captured(0).size()==color.size()) {
QColor col=jkqtp_lookupQColorName(mFrac.captured(1));
const double a=static_cast<double>(QLocale::c().toInt(mFrac.captured(2)))/100.0;
if (mFrac.captured(1)=="grey"||mFrac.captured(1)=="gray") col.setRgbF(a,a,a);
@ -744,62 +743,10 @@ QColor jkqtp_String2QColor(QString color)
return col;
}
#else
QRegExp rxP("(.+)\\s*,\\s*t?\\s*(\\d+\\.?\\d+)\\%");
QRegExp rxAP("(.+)\\s*,\\s*a\\s*(\\d+\\.?\\d+)\\%");
QRegExp rxNP("(.+)\\s*,\\s*a?\\s*([\\d]+)");
QRegExp rxP("([^,\\(\\)]+)\\s*,\\s*t?\\s*(\\d+\\.?\\d+)\\%");
QRegExp rxAP("([^,\\(\\)]+)\\s*,\\s*a\\s*(\\d+\\.?\\d+)\\%");
QRegExp rxNP("([^,\\(\\)]+)\\s*,\\s*a?\\s*([\\d]+)");
QRegExp rxFrac("([a-zA-Z]{3,})(\\d{1,3})\\%?");
QRegExp rxColMod("\\s*(rgb|hsl|hsv|rgba|gray|grey|red|green|blue)\\(\\s*(\\d+\\.?\\d*)(%|deg)?(?:\\s+|\\s*,\\s*|\\s\\/\\s)?(\\d+\\.?\\d*)?(%)?(?:\\s+|\\s*,\\s*|\\s\\/\\s)?(\\d+\\.?\\d*)?(%)?(?:\\s+|\\s*,\\s*|\\s*\\/\\s*)?(\\d+\\.?\\d*)?(%)?\\s*\\)\\s*");
if (rxColMod.exactMatch(color)) {
QColor col(Qt::black);
const QString name=rxColMod.cap(1);
const int v1=valUnitToInt(rxColMod.cap(2), rxColMod.cap(3));
const int h1=valUnitToInt(rxColMod.cap(2), rxColMod.cap(3), INT_MAX);
const int v2=valUnitToInt(rxColMod.cap(4), rxColMod.cap(5));
const int a2=valUnitToAlphaInt(rxColMod.cap(4), rxColMod.cap(5));
const int v3=valUnitToInt(rxColMod.cap(6), rxColMod.cap(7));
const int a4=valUnitToAlphaInt(rxColMod.cap(8), rxColMod.cap(9));
if (name=="gray"||name=="grey") {
if (v2<0) col.setRgb(v1,v1,v1);
else col.setRgb(v1,v1,v1,a2);
return col;
}
else if (name=="red") {
col.setRed(v1);
if (v2>=0) col.setAlpha(a2);
return col;
}
else if (name=="green") {
col.setGreen(v1);
if (v2>=0) col.setAlpha(a2);
return col;
}
else if (name=="blue") {
col.setBlue(v1);
if (v2>=0) col.setAlpha(a2);
return col;
}
else if (name=="rgb") {
if (a4<0) col.setRgb(v1,v2,v3);
else col.setRgb(v1,v2,v3,a2);
return col;
}
else if (name=="rgba") {
col.setRgb(v1,v2,v3,a2);
return col;
}
else if (name=="hsl") {
if (a4<0) col.setHsl(h1,v2,v3);
else col.setHsl(h1,v2,v3,a2);
return col;
}
else if (name=="hsv") {
if (a4<0) col.setHsv(h1,v2,v3);
else col.setHsv(h1,v2,v3,a2);
return col;
} else {
qDebug()<<"unrecognized CSS-color:'"<<color<<"'";
}
}
if (rxP.exactMatch(color)) {
QColor col=jkqtp_lookupQColorName(rxP.cap(1));
double a=QLocale::c().toDouble(rxP.cap(2));
@ -826,7 +773,13 @@ QColor jkqtp_String2QColor(QString color)
return col;
}
#endif
return jkqtp_lookupQColorName(color);
try {
return JKQTPCSSParser::readColor(color);
} catch(std::exception& E) {
qWarning()<<"error converting '"<<color<<"' into a QColor: "<<E.what();
return jkqtp_lookupQColorName(color);
}
}
std::string jkqtp_to_valid_variable_name(const std::string& input) {
@ -1092,3 +1045,125 @@ QString jkqtp_backslashEscape(const QString &txt)
}
return res;
}
namespace JKQTCommon_private {
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
thread_local QCache<QString, QRegularExpression> rxCache(500);
#else
thread_local QCache<QString, QRegExp> rxCache(500);
#endif
}
bool jkqtp_rxContains(const QString& text, const QString &regex, qsizetype offset, QStringList* caps)
{
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegularExpression(regex));
QRegularExpression* rx=JKQTCommon_private::rxCache.object(regex);
QSharedPointer<QRegularExpression> tempRX;
if (!rx) {
tempRX.reset(new QRegularExpression(regex));
rx=tempRX.data();
}
const auto m=rx->match(text, offset);
if (caps) *caps=m.capturedTexts();
return m.hasMatch();
#else
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegExp(regex));
QRegExp* rx=JKQTCommon_private::rxCache.object(regex);
QSharedPointer<QRegExp> tempRX;
if (!rx) {
tempRX.reset(new QRegExp(regex));
rx=tempRX.data();
}
const bool res= rx->indexIn(text, offset)>=0;
if (caps) *caps=rx->capturedTexts();
return res;
#endif
}
qsizetype jkqtp_rxIndexIn(const QString& text, const QString &regex, qsizetype offset, QStringList* caps)
{
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegularExpression(regex));
QRegularExpression* rx=JKQTCommon_private::rxCache.object(regex);
QSharedPointer<QRegularExpression> tempRX;
if (!rx) {
tempRX.reset(new QRegularExpression(regex));
rx=tempRX.data();
}
QRegularExpressionMatch rmatch;
const qsizetype idx=text.indexOf(*rx, offset, &rmatch);
if (caps) *caps=rmatch.capturedTexts();
return idx;
#else
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegExp(regex));
QRegExp* rx=JKQTCommon_private::rxCache.object(regex);
QSharedPointer<QRegExp> tempRX;
if (!rx) {
tempRX.reset(new QRegExp(regex));
rx=tempRX.data();
}
const qsizetype idx = rx->indexIn(text, offset);
if (caps) *caps=rx->capturedTexts();
return idx;
#endif
}
bool jkqtp_rxPartiallyMatchesAt(const QString& text, const QString &regex, qsizetype offset, QStringList* caps)
{
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegularExpression(regex));
QRegularExpression* rx=JKQTCommon_private::rxCache.object(regex);
QSharedPointer<QRegularExpression> tempRX;
if (!rx) {
tempRX.reset(new QRegularExpression(regex));
rx=tempRX.data();
}
const auto m=rx->match(text, offset, QRegularExpression::NormalMatch, QRegularExpression::AnchorAtOffsetMatchOption);
if (caps) *caps=m.capturedTexts();
return m.hasMatch();
#else
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegExp(regex));
QRegExp* rx=JKQTCommon_private::rxCache.object(regex);
QSharedPointer<QRegExp> tempRX;
if (!rx) {
tempRX.reset(new QRegExp(regex));
rx=tempRX.data();
}
const bool res= (rx->indexIn(text, offset)==offset);
if (caps) *caps=rx->capturedTexts();
return res;
#endif
}
bool jkqtp_rxExactlyMatches(const QString& text, const QString &regex, QStringList* caps)
{
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegularExpression(regex));
QRegularExpression* rx=JKQTCommon_private::rxCache.object(regex);
QSharedPointer<QRegularExpression> tempRX;
if (!rx) {
tempRX.reset(new QRegularExpression(regex));
rx=tempRX.data();
}
const auto m=rx->match(text);
if (caps) *caps=m.capturedTexts();
return m.hasMatch() && m.captured(0).size()==text.size();
#else
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegExp(regex));
QRegExp* rx=JKQTCommon_private::rxCache.object(regex);
QSharedPointer<QRegExp> tempRX;
if (!rx) {
tempRX.reset(new QRegExp(regex));
rx=tempRX.data();
}
const bool res= rx->exactMatch(text);
if (caps) *caps=rx->capturedTexts();
return res;
#endif
}

View File

@ -28,6 +28,8 @@
#include <QtGlobal>
#include <limits>
class QGradient; // forward
/** \brief converts a QT::PenStyle into a string
* \ingroup jkqtptools_string
*/
@ -45,9 +47,24 @@ JKQTCOMMON_LIB_EXPORT QString jkqtp_QBrushStyle2String(Qt::BrushStyle style);
/** \brief converts a QString into a Qt::BrushStyle
* \ingroup jkqtptools_string
* \see jkqtp_String2QBrushStyleExt() for a more complex parser
*/
JKQTCOMMON_LIB_EXPORT Qt::BrushStyle jkqtp_String2QBrushStyle(const QString& style);
/** \brief converts a QString into a Qt::BrushStyle. commpared to jkqtp_String2QBrushStyle(), this method can parse
* more complex pattern/brush descriptions, such as for colors, gradients or images, which are output in \a color, \a gradient and \a image
* \ingroup jkqtptools_string
*
* \param style the string to be parsed
* \param[out] color output parameter for a parsed color
* \param[out] gradient output parameter for a parsed gradient
* \param[out] image output parameter for a parsed image
* \param[out] rotationAngleDeg output parameter for a parsed rotation angle of the pattern in degrees, where the direction equals the direction of a clock hand, i.e. 0=12o'clock, 180=6o'clock, ...
*
* \see jkqtp_String2QBrushStyle()
*/
JKQTCOMMON_LIB_EXPORT Qt::BrushStyle jkqtp_String2QBrushStyleExt(const QString& style, QColor* color=nullptr, QGradient* gradient=nullptr, QPixmap* image=nullptr, double *rotationAngleDeg=nullptr);
/** \brief converts a Unicode codepoint into a UTF8-sequence
* \ingroup jkqtptools_string
@ -326,5 +343,51 @@ JKQTCOMMON_LIB_EXPORT QString jkqtp_MouseButton2String(Qt::MouseButton button, b
*/
JKQTCOMMON_LIB_EXPORT Qt::MouseButton jkqtp_String2MouseButton(const QString &button);
/** \brief returns \c true, if \a text contains a match to the given regular expression \a regex,
* starts from \a offset and optionally returns the match in \a caps \c =[fullmatch, cap1,cap2,...]
* \ingroup jkqtptools_string
*
* \note this function uses an internal cache, so the \a regex does not have
* to be compiled every time this is called (with the same \a regex ).
*
* \see jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt()
*/
JKQTCOMMON_LIB_EXPORT bool jkqtp_rxContains(const QString& text, const QString &regex, qsizetype offset=0, QStringList* caps=nullptr);
/** \brief returns the next match (i.e. its index) of the given regular expression \a regex within \a text,
* starts from \a offset and optionally returns the match in \a caps \c =[fullmatch, cap1,cap2,...]
* \ingroup jkqtptools_string
*
* \note this function uses an internal cache, so the \a regex does not have
* to be compiled every time this is called (with the same \a regex ).
*
* \see jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt()
*/
JKQTCOMMON_LIB_EXPORT qsizetype jkqtp_rxIndexIn(const QString& text, const QString &regex, qsizetype offset=0, QStringList* caps=nullptr);
/** \brief returns \c true, if \a text exactly matches the given regular expression \a regex,
* starts from \a offset and optionally returns the match in \a caps \c =[fullmatch, cap1,cap2,...]
* \ingroup jkqtptools_string
*
* \note this function uses an internal cache, so the \a regex does not have
* to be compiled every time this is called (with the same \a regex ).
*
* \see jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt()
*/
JKQTCOMMON_LIB_EXPORT bool jkqtp_rxExactlyMatches(const QString& text, const QString &regex, QStringList* caps=nullptr);
/** \brief returns \c true, if \a text partially matches the given regular expression \a regex,
* starting from \a offset (and the match starts at \a offset !!!)
* and optionally returns the match in \a caps \c =[fullmatch, cap1,cap2,...]
* \ingroup jkqtptools_string
*
* \note this function uses an internal cache, so the \a regex does not have
* to be compiled every time this is called (with the same \a regex ).
*
* \see jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt()
*/
JKQTCOMMON_LIB_EXPORT bool jkqtp_rxPartiallyMatchesAt(const QString& text, const QString &regex, qsizetype offset=0, QStringList* caps=nullptr);
#endif // JKQTPSTRINGTOOLS_H_INCLUDED