/* Copyright (c) 2008-2024 Jan W. Krieger () 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 . */ #include "jkqtcommon/jkqtpcsstools.h" #include "jkqtcommon/jkqtpstringtools.h" #include "jkqtcommon/jkqtpcodestructuring.h" #include #include #include #include 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::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 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 > rgbFuncs={ {"rgb", QPair(3,4)}, {"rgba",QPair(4,4)}, {"hsl",QPair(3,4)}, {"hsv",QPair(3,4)}, {"gray",QPair(1,2)}, {"grey",QPair(1,2)}, {"red",QPair(1,2)}, {"green",QPair(1,2)}, {"blue",QPair(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 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 JKQTPCSSParser::parseGradient(bool get) { QGradient grad=QGradient(); if (get) getToken(); static QMap s_GradientPresets = []() { QMap m; for (int i=1; i().valueToKey(static_cast(i))).toLower().trimmed().simplified(); if (id.size()>0) m[id]=static_cast(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 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<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(cnt+1); for (int j=lastI+1; j(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_): type(NUMBER), StringValue(unit_), NumberValue(num) { } JKQTPCSSParser::Token::Token(const QString &str, TokenType type_): type(type_), StringValue(str), NumberValue(0.0) { 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()); }