438 lines
17 KiB
C++
438 lines
17 KiB
C++
/*******************************************
|
|
* Zira Editor
|
|
* A lightweight PHP Editor
|
|
* (C)2019 https://github.com/ziracms/editor
|
|
*******************************************/
|
|
|
|
#include "parsecss.h"
|
|
#include <QFile>
|
|
#include <QTextStream>
|
|
#include "helper.h"
|
|
|
|
const int EXPECT_SELECTOR = 0;
|
|
const int EXPECT_MEDIA = 1;
|
|
const int EXPECT_KEYFRAMES = 2;
|
|
const int EXPECT_FONT_FACE = 3;
|
|
const int EXPECT_FONT_FAMILY = 4;
|
|
|
|
std::unordered_map<std::string, std::string> ParseCSS::mainTags = {};
|
|
|
|
ParseCSS::ParseCSS()
|
|
{
|
|
parseExpression = QRegularExpression("([a-zA-Z0-9_\\-]+|[\\$\\(\\)\\{\\}\\[\\]\\.,=;:!@#%^&*+/\\|<>\\?\\\\])", QRegularExpression::DotMatchesEverythingOption);
|
|
nameExpression = QRegularExpression("^[#\\.]?[a-zA-Z_][a-zA-Z0-9_\\-#\\.: ]*$");
|
|
colorExpression = QRegularExpression("^[#](?:[a-fA-F0-9][a-fA-F0-9][a-fA-F0-9])(?:[a-fA-F0-9][a-fA-F0-9][a-fA-F0-9])?(?:[a-fA-F0-9][a-fA-F0-9])?$");
|
|
|
|
if (mainTags.size() == 0) {
|
|
QFile sf(":/syntax/html_alltags");
|
|
sf.open(QIODevice::ReadOnly);
|
|
QTextStream sin(&sf);
|
|
QString k;
|
|
while (!sin.atEnd()) {
|
|
k = sin.readLine();
|
|
if (k == "") continue;
|
|
mainTags[k.toStdString()] = k.toStdString();
|
|
}
|
|
sf.close();
|
|
}
|
|
}
|
|
|
|
QString ParseCSS::cleanUp(QString text)
|
|
{
|
|
comments.clear();
|
|
prepare(text);
|
|
// strip strings & comments
|
|
int offset = 0;
|
|
QList<int> matchesPos;
|
|
QRegularExpressionMatch stringDQMatch;
|
|
QRegularExpressionMatch stringSQMatch;
|
|
QRegularExpressionMatch commentMLMatch;
|
|
int stringDQPos = -2,
|
|
stringSQPos = -2,
|
|
commentMLPos = -2;
|
|
do {
|
|
matchesPos.clear();
|
|
if (stringDQPos != -1 && stringDQPos < offset) {
|
|
stringDQMatch = stringDQExpression.match(text, offset);
|
|
stringDQPos = stringDQMatch.capturedStart(1)-1;
|
|
}
|
|
if (stringDQPos >= 0) matchesPos.append(stringDQPos);
|
|
if (stringSQPos != -1 && stringSQPos < offset) {
|
|
stringSQMatch = stringSQExpression.match(text, offset);
|
|
stringSQPos = stringSQMatch.capturedStart(1)-1;
|
|
}
|
|
if (stringSQPos >= 0) matchesPos.append(stringSQPos);
|
|
if (commentMLPos != -1 && commentMLPos < offset) {
|
|
commentMLMatch = commentMLExpression.match(text, offset);
|
|
commentMLPos = commentMLMatch.capturedStart();
|
|
}
|
|
if (commentMLPos >= 0) matchesPos.append(commentMLPos);
|
|
if (matchesPos.size() == 0) break;
|
|
std::sort(matchesPos.begin(), matchesPos.end());
|
|
int pos = matchesPos.at(0);
|
|
if (stringDQPos == pos) {
|
|
offset = stringDQMatch.capturedStart() + stringDQMatch.capturedLength();
|
|
strip(stringDQMatch, text, 1);
|
|
continue;
|
|
}
|
|
if (stringSQPos == pos) {
|
|
offset = stringSQMatch.capturedStart() + stringSQMatch.capturedLength();
|
|
strip(stringSQMatch, text, 1);
|
|
continue;
|
|
}
|
|
if (commentMLPos == pos) {
|
|
offset = commentMLMatch.capturedStart() + commentMLMatch.capturedLength();
|
|
QString stripped = strip(commentMLMatch, text, 0); // group 0
|
|
comments[getLine(text, offset)] = stripped.toStdString();
|
|
continue;
|
|
}
|
|
} while (matchesPos.size() > 0);
|
|
return text;
|
|
}
|
|
|
|
bool ParseCSS::isValidName(QString name)
|
|
{
|
|
QRegularExpressionMatch m = nameExpression.match(name);
|
|
return (m.capturedStart()==0);
|
|
}
|
|
|
|
bool ParseCSS::isColor(QString name)
|
|
{
|
|
QRegularExpressionMatch m = colorExpression.match(name);
|
|
return (m.capturedStart()==0);
|
|
}
|
|
|
|
void ParseCSS::addSelector(QString name, int line) {
|
|
if (!isValidName(name)) return;
|
|
selectorIndexesIterator = selectorIndexes.find(name.toStdString());
|
|
if (selectorIndexesIterator != selectorIndexes.end()) return;
|
|
ParseResultSelector selector;
|
|
selector.name = name;
|
|
selector.line = line;
|
|
result.selectors.append(selector);
|
|
selectorIndexes[name.toStdString()] = result.selectors.size() - 1;
|
|
}
|
|
|
|
void ParseCSS::addName(QString name, int line) {
|
|
if (!isValidName(name) || isColor(name)) return;
|
|
nameIndexesIterator = nameIndexes.find(name.toStdString());
|
|
if (nameIndexesIterator != nameIndexes.end()) return;
|
|
ParseResultName nm;
|
|
nm.name = name;
|
|
nm.line = line;
|
|
result.names.append(nm);
|
|
nameIndexes[name.toStdString()] = result.names.size() - 1;
|
|
}
|
|
|
|
void ParseCSS::addMedia(QString name, int line) {
|
|
if (!isValidName(name)) return;
|
|
mediaIndexesIterator = mediaIndexes.find(name.toStdString());
|
|
if (mediaIndexesIterator != mediaIndexes.end()) return;
|
|
ParseResultMedia media;
|
|
media.name = name;
|
|
media.line = line;
|
|
result.medias.append(media);
|
|
mediaIndexes[name.toStdString()] = result.medias.size() - 1;
|
|
}
|
|
|
|
void ParseCSS::addKeyframe(QString name, int line) {
|
|
if (!isValidName(name)) return;
|
|
keyframeIndexesIterator = keyframeIndexes.find(name.toStdString());
|
|
if (keyframeIndexesIterator != keyframeIndexes.end()) return;
|
|
ParseResultKeyframe keyframe;
|
|
keyframe.name = name;
|
|
keyframe.line = line;
|
|
result.keyframes.append(keyframe);
|
|
keyframeIndexes[name.toStdString()] = result.keyframes.size() - 1;
|
|
}
|
|
|
|
void ParseCSS::addFont(QString name, int line) {
|
|
if (!isValidName(name)) return;
|
|
fontIndexesIterator = fontIndexes.find(name.toStdString());
|
|
if (fontIndexesIterator != fontIndexes.end()) return;
|
|
ParseResultFont font;
|
|
font.name = name;
|
|
font.line = line;
|
|
result.fonts.append(font);
|
|
fontIndexes[name.toStdString()] = result.fonts.size() - 1;
|
|
}
|
|
|
|
void ParseCSS::addComment(QString text, int line) {
|
|
QString name = "";
|
|
if (text.size() > 0) {
|
|
text.replace(QRegularExpression("^[/][*](.*)[*][/]$", QRegularExpression::DotMatchesEverythingOption), "\\1");
|
|
QString text_clean = "";
|
|
QStringList textList = text.split("\n");
|
|
for (int i=0; i<textList.size(); i++) {
|
|
QString text_line = textList.at(i).trimmed().replace(QRegularExpression("^[*]+[\\s]*"), "").replace(QRegularExpression("[\\s]*[*]+$"), "");
|
|
if (text_line.size() == 0) continue;
|
|
if (text_clean.size() > 0) text_clean += "\n";
|
|
if (name.size() == 0) name = text_line;
|
|
text_clean += text_line;
|
|
}
|
|
text = text_clean;
|
|
}
|
|
if (text.size() == 0 || name.size() == 0) return;
|
|
ParseResultComment comment;
|
|
comment.name = name;
|
|
comment.text = text;
|
|
comment.line = line;
|
|
result.comments.append(comment);
|
|
}
|
|
|
|
void ParseCSS::addError(QString text, int line, int symbol) {
|
|
ParseResultError error;
|
|
error.text = text;
|
|
error.line = line;
|
|
error.symbol = symbol;
|
|
result.errors.append(error);
|
|
}
|
|
|
|
void ParseCSS::parseCode(QString & code, QString & origText)
|
|
{
|
|
// parse data
|
|
QString current_selector = "";
|
|
QString current_media = "";
|
|
QString current_keyframe = "";
|
|
QString current_font = "";
|
|
|
|
int scope = 0;
|
|
int selectorScope = -1, mediaScope = -1, keyframeScope = -1, fontScope = -1;
|
|
int pars = 0;
|
|
int curlyBrackets = 0, roundBrackets = 0, squareBrackets = 0;
|
|
QVector<int> curlyBracketsList, roundBracketsList, squareBracketsList;
|
|
int mediaArgPars = -1;
|
|
int mediaArgsStart = -1;
|
|
int fontFamilyStart = -1;
|
|
int expect = -1;
|
|
QString expectName = "";
|
|
QString prevK = "", prevPrevK = "", prevPrevPrevK = "", prevPrevPrevPrevK = "", prevPrevPrevPrevPrevK = "", prevPrevPrevPrevPrevPrevK = "", prevPrevPrevPrevPrevPrevPrevK = "", prevPrevPrevPrevPrevPrevPrevPrevK = "", prevPrevPrevPrevPrevPrevPrevPrevPrevK = "";
|
|
int selectorStart = -1, mediaStart = -1, keyframeStart = -1, fontStart = -1;
|
|
|
|
QRegularExpressionMatchIterator mi = parseExpression.globalMatch(code);
|
|
while(mi.hasNext()){
|
|
QRegularExpressionMatch m = mi.next();
|
|
if (m.capturedStart(1) < 0) continue;
|
|
QString k = m.captured(1).trimmed();
|
|
if (k.size() == 0) continue;
|
|
|
|
// main tags selectors
|
|
if (expect < 0 && scope == 0 && expect != EXPECT_SELECTOR && k.size() > 1 && (prevK.size() == 0 || prevK == "}") && current_selector.size() == 0) {
|
|
mainTagsIterator = mainTags.find(k.toLower().toStdString());
|
|
if (mainTagsIterator != mainTags.end()) {
|
|
expect = EXPECT_SELECTOR;
|
|
expectName = k;
|
|
selectorStart = m.capturedStart(1);
|
|
current_selector = "";
|
|
}
|
|
} else if (expect == EXPECT_SELECTOR && expectName.size() > 0 && prevK == expectName && (k == "#" || k == ".") && m.capturedStart(1) == selectorStart+expectName.size()) {
|
|
expectName += k;
|
|
} else if (expect == EXPECT_SELECTOR && expectName.size() > 0 && prevPrevK + prevK == expectName && (prevK == "#" || prevK == ".") && k != "," && k != "{") {
|
|
expectName += k;
|
|
} else if (expect == EXPECT_SELECTOR && expectName.size() > 0 && k == "{" && mediaArgPars < 0) {
|
|
current_selector = expectName;
|
|
int line = 0;
|
|
if (selectorStart >= 0) line = getLine(origText, selectorStart);
|
|
addSelector(current_selector, line);
|
|
selectorScope = scope;
|
|
expect = -1;
|
|
expectName = "";
|
|
}
|
|
|
|
// ids & classes
|
|
if ((prevK == "#" || prevK == ".") && k.size() > 0 && pars == 0) {
|
|
int line = getLine(origText, m.capturedStart(1));
|
|
QString name = prevK + k;
|
|
addName(name, line);
|
|
}
|
|
|
|
// media
|
|
if (expect < 0 && scope == 0 && expect != EXPECT_MEDIA && k.toLower() == "media" && prevK == "@" && current_media.size() == 0) {
|
|
expect = EXPECT_MEDIA;
|
|
expectName = "";
|
|
mediaStart = m.capturedStart(1);
|
|
mediaArgPars = -1;
|
|
mediaArgsStart = -1;
|
|
current_media = "";
|
|
} else if (expect == EXPECT_MEDIA && expectName.size() == 0 && k == "(") {
|
|
mediaArgPars = pars;
|
|
mediaArgsStart = m.capturedStart(1);
|
|
} else if (expect == EXPECT_MEDIA && expectName.size() > 0 && k == "{" && mediaArgPars < 0) {
|
|
current_media = expectName;
|
|
int line = 0;
|
|
if (mediaStart >= 0) line = getLine(origText, mediaStart);
|
|
addMedia(current_media, line);
|
|
mediaScope = scope;
|
|
mediaArgPars = -1;
|
|
mediaArgsStart = -1;
|
|
expect = -1;
|
|
expectName = "";
|
|
}
|
|
|
|
// keyframes
|
|
if (expect < 0 && scope == 0 && expect != EXPECT_KEYFRAMES && prevK.toLower() == "keyframes" && prevPrevK == "@" && k.size() > 0 && current_keyframe.size() == 0) {
|
|
expect = EXPECT_KEYFRAMES;
|
|
expectName = k;
|
|
keyframeStart = m.capturedStart(1);
|
|
current_keyframe = "";
|
|
} else if (expect == EXPECT_KEYFRAMES && expectName.size() > 0 && k == "{" && mediaArgPars < 0) {
|
|
current_keyframe = expectName;
|
|
int line = 0;
|
|
if (keyframeStart >= 0) line = getLine(origText, keyframeStart);
|
|
addKeyframe(current_keyframe, line);
|
|
keyframeScope = scope;
|
|
expect = -1;
|
|
expectName = "";
|
|
}
|
|
|
|
// font-face
|
|
if (expect < 0 && scope == 0 && expect != EXPECT_FONT_FACE && prevK.toLower() == "font-face" && prevPrevK == "@" && k == "{" && current_font.size() == 0) {
|
|
expect = EXPECT_FONT_FACE;
|
|
expectName = "";
|
|
fontStart = m.capturedStart(1);
|
|
fontScope = scope;
|
|
fontFamilyStart = -1;
|
|
current_font = "";
|
|
} else if (expect != EXPECT_FONT_FACE && current_font.size() == 0 && fontScope >= 0 && fontFamilyStart < 0 && prevK.toLower() == "font-family" && k == ":") {
|
|
expect = EXPECT_FONT_FAMILY;
|
|
fontFamilyStart = m.capturedStart(1);
|
|
} else if (expect == EXPECT_FONT_FAMILY && current_font.size() == 0 && k == ";" && fontFamilyStart >= 0 && mediaArgPars < 0) {
|
|
current_font = origText.mid(fontFamilyStart+1, m.capturedStart(1)-fontFamilyStart-1).trimmed().replace("\"","").replace("'","").replace(QRegularExpression("[\\s]+"), " ");
|
|
int line = 0;
|
|
if (fontStart >= 0) line = getLine(origText, fontStart);
|
|
addFont(current_font, line);
|
|
expect = -1;
|
|
expectName = "";
|
|
}
|
|
|
|
if ((k == ";" || k == "{" || k == "}") && mediaArgsStart < 0) {
|
|
expect = -1;
|
|
expectName = "";
|
|
}
|
|
// braces
|
|
if (k == "{") {
|
|
scope++;
|
|
curlyBrackets++;
|
|
curlyBracketsList.append(m.capturedStart(1)+1);
|
|
}
|
|
if (k == "}") {
|
|
scope--;
|
|
if (scope < 0) scope = 0;
|
|
curlyBrackets--;
|
|
curlyBracketsList.append(-1 * (m.capturedStart(1)+1));
|
|
// selector close
|
|
if (current_selector.size() > 0 && selectorScope >= 0 && selectorScope == scope) {
|
|
current_selector = "";
|
|
selectorScope = -1;
|
|
}
|
|
// media close
|
|
if (current_media.size() > 0 && mediaScope >= 0 && mediaScope == scope) {
|
|
current_media = "";
|
|
mediaScope = -1;
|
|
}
|
|
// keyframes close
|
|
if (current_keyframe.size() > 0 && keyframeScope >= 0 && keyframeScope == scope) {
|
|
current_keyframe = "";
|
|
keyframeScope = -1;
|
|
}
|
|
// font-face close
|
|
if (fontScope >= 0 && fontScope == scope) {
|
|
current_font = "";
|
|
fontScope = -1;
|
|
}
|
|
}
|
|
// parens
|
|
if (k == "(") {
|
|
pars++;
|
|
roundBrackets++;
|
|
roundBracketsList.append(m.capturedStart(1)+1);
|
|
}
|
|
if (k == ")") {
|
|
pars--;
|
|
if (pars < 0) pars = 0;
|
|
roundBrackets--;
|
|
roundBracketsList.append(-1 * (m.capturedStart(1)+1));
|
|
// media args
|
|
if (mediaArgPars >= 0 && mediaArgPars == pars && mediaArgsStart >= 0 && expectName.size() == 0) {
|
|
expectName = origText.mid(mediaArgsStart+1, m.capturedStart(1)-mediaArgsStart-1).trimmed().replace(QRegularExpression("[\\s]+"), " ");
|
|
mediaArgPars = -1;
|
|
mediaArgsStart = -1;
|
|
}
|
|
}
|
|
// brackets
|
|
if (k == "[") {
|
|
squareBrackets++;
|
|
squareBracketsList.append(m.capturedStart(1)+1);
|
|
}
|
|
if (k == "]") {
|
|
squareBrackets--;
|
|
squareBracketsList.append(-1 * (m.capturedStart(1)+1));
|
|
}
|
|
prevPrevPrevPrevPrevPrevPrevPrevPrevK = prevPrevPrevPrevPrevPrevPrevPrevK;
|
|
prevPrevPrevPrevPrevPrevPrevPrevK = prevPrevPrevPrevPrevPrevPrevK;
|
|
prevPrevPrevPrevPrevPrevPrevK = prevPrevPrevPrevPrevPrevK;
|
|
prevPrevPrevPrevPrevPrevK = prevPrevPrevPrevPrevK;
|
|
prevPrevPrevPrevPrevK = prevPrevPrevPrevK;
|
|
prevPrevPrevPrevK = prevPrevPrevK;
|
|
prevPrevPrevK = prevPrevK;
|
|
prevPrevK = prevK;
|
|
prevK = k;
|
|
}
|
|
if (curlyBrackets > 0) {
|
|
int offset = findOpenScope(curlyBracketsList);
|
|
if (offset != 0) offset = abs(offset) - 1;
|
|
int line = getLine(origText, offset);
|
|
addError(QObject::tr("Unclosed brace"), line, offset);
|
|
} else if (curlyBrackets < 0) {
|
|
int offset = findCloseScope(curlyBracketsList);
|
|
if (offset != 0) offset = abs(offset) - 1;
|
|
int line = getLine(origText, offset);
|
|
addError(QObject::tr("Excess brace"), line, offset);
|
|
}
|
|
if (roundBrackets > 0) {
|
|
int offset = findOpenScope(roundBracketsList);
|
|
if (offset != 0) offset = abs(offset) - 1;
|
|
int line = getLine(origText, offset);
|
|
addError(QObject::tr("Unclosed parenthesis"), line, offset);
|
|
} else if (roundBrackets < 0) {
|
|
int offset = findCloseScope(roundBracketsList);
|
|
if (offset != 0) offset = abs(offset) - 1;
|
|
int line = getLine(origText, offset);
|
|
addError(QObject::tr("Excess parenthesis"), line, offset);
|
|
}
|
|
if (squareBrackets > 0) {
|
|
int offset = findOpenScope(squareBracketsList);
|
|
if (offset != 0) offset = abs(offset) - 1;
|
|
int line = getLine(origText, offset);
|
|
addError(QObject::tr("Unclosed bracket"), line, offset);
|
|
} else if (squareBrackets < 0) {
|
|
int offset = findCloseScope(squareBracketsList);
|
|
if (offset != 0) offset = abs(offset) - 1;
|
|
int line = getLine(origText, offset);
|
|
addError(QObject::tr("Excess bracket"), line, offset);
|
|
}
|
|
}
|
|
|
|
void ParseCSS::reset()
|
|
{
|
|
selectorIndexes.clear();
|
|
mediaIndexes.clear();
|
|
keyframeIndexes.clear();
|
|
fontIndexes.clear();
|
|
comments.clear();
|
|
}
|
|
|
|
ParseCSS::ParseResult ParseCSS::parse(QString text)
|
|
{
|
|
result = ParseResult();
|
|
reset();
|
|
QString cleanText = cleanUp(text);
|
|
parseCode(cleanText, text);
|
|
// comments
|
|
std::map<int, std::string> orderedComments(comments.begin(), comments.end());
|
|
for (auto & commentsIterator : orderedComments) {
|
|
addComment(QString::fromStdString(commentsIterator.second), commentsIterator.first);
|
|
}
|
|
return result;
|
|
}
|