diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e00625c..e00de419b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unversioned - Major: Added clip creation support. You can create clips with `/clip` command, `Alt+X` keybind or `Create a clip` option in split header's context menu. This requires a new authentication scope so re-authentication will be required to use it. (#2271, #2377) -- Major: Added "Channel Filters". See https://wiki.chatterino.com/Filters/ for how they work or how to configure them. (#1748, #2083, #2090, #2200) +- Major: Added "Channel Filters". See https://wiki.chatterino.com/Filters/ for how they work or how to configure them. (#1748, #2083, #2090, #2200, #2225) - Major: Added Streamer Mode configuration (under `Settings -> General`), where you can select which features of Chatterino should behave differently when you are in Streamer Mode. (#2001, #2316, #2342, #2376) - Major: Color mentions to match the mentioned users. You can disable this by unchecking "Color @usernames" under `Settings -> General -> Advanced (misc.)`. (#1963, #2284) - Minor: Added `/marker` command - similar to webchat, it creates a stream marker. (#2360) diff --git a/src/controllers/filters/parser/FilterParser.cpp b/src/controllers/filters/parser/FilterParser.cpp index d79ea6a6c..1922dfb24 100644 --- a/src/controllers/filters/parser/FilterParser.cpp +++ b/src/controllers/filters/parser/FilterParser.cpp @@ -255,6 +255,16 @@ ExpressionPtr FilterParser::parseValue() return std::make_unique(this->tokenizer_.next(), type); } + else if (type == TokenType::REGULAR_EXPRESSION) + { + auto before = this->tokenizer_.next(); + // remove quote marks and r/ri + bool caseInsensitive = before.startsWith("ri"); + auto val = before.mid(caseInsensitive ? 3 : 2); + val.chop(1); + val = val.replace("\\\"", "\""); + return std::make_unique(val, caseInsensitive); + } else if (type == TokenType::LP) { return this->parseParentheses(); diff --git a/src/controllers/filters/parser/Tokenizer.cpp b/src/controllers/filters/parser/Tokenizer.cpp index d3e6f3b14..cc00697e0 100644 --- a/src/controllers/filters/parser/Tokenizer.cpp +++ b/src/controllers/filters/parser/Tokenizer.cpp @@ -141,10 +141,18 @@ TokenType Tokenizer::tokenize(const QString &text) return TokenType::STARTS_WITH; else if (text == "endswith") return TokenType::ENDS_WITH; + else if (text == "match") + return TokenType::MATCH; else if (text == "!") return TokenType::NOT; else { + if ((text.startsWith("r\"") || text.startsWith("ri\"")) && + text.back() == '"') + { + return TokenType::REGULAR_EXPRESSION; + } + if (text.front() == '"' && text.back() == '"') return TokenType::STRING; diff --git a/src/controllers/filters/parser/Tokenizer.hpp b/src/controllers/filters/parser/Tokenizer.hpp index 38a0a1cb7..78ff27064 100644 --- a/src/controllers/filters/parser/Tokenizer.hpp +++ b/src/controllers/filters/parser/Tokenizer.hpp @@ -27,7 +27,7 @@ static const QMap validIdentifiersMap = { // clang-format off static const QRegularExpression tokenRegex( - QString("\\\"((\\\\\")|[^\\\"])*\\\"|") + // String literal + QString("((r|ri)?\\\")((\\\\\")|[^\\\"])*\\\"|") + // String/Regex literal QString("[\\w\\.]+|") + // Identifier or reserved keyword QString("(<=?|>=?|!=?|==|\\|\\||&&|\\+|-|\\*|\\/|%)+|") + // Operator QString("[\\(\\)]|") + // Parentheses diff --git a/src/controllers/filters/parser/Types.cpp b/src/controllers/filters/parser/Types.cpp index 497c2f55e..159e89ce9 100644 --- a/src/controllers/filters/parser/Types.cpp +++ b/src/controllers/filters/parser/Types.cpp @@ -69,6 +69,8 @@ QString tokenTypeToInfoString(TokenType type) return ""; case ENDS_WITH: return ""; + case MATCH: + return ""; case NOT: return ""; case STRING: @@ -123,6 +125,33 @@ QString ValueExpression::filterString() const } } +// RegexExpression + +RegexExpression::RegexExpression(QString regex, bool caseInsensitive) + : regexString_(regex) + , caseInsensitive_(caseInsensitive) + , regex_(QRegularExpression( + regex, caseInsensitive ? QRegularExpression::CaseInsensitiveOption + : QRegularExpression::NoPatternOption)){}; + +QVariant RegexExpression::execute(const ContextMap &) const +{ + return this->regex_; +} + +QString RegexExpression::debug() const +{ + return this->regexString_; +} + +QString RegexExpression::filterString() const +{ + auto s = this->regexString_; + return QString("%1\"%2\"") + .arg(this->caseInsensitive_ ? "ri" : "r") + .arg(s.replace("\"", "\\\"")); +} + // ListExpression ListExpression::ListExpression(ExpressionList list) @@ -334,6 +363,47 @@ QVariant BinaryOperation::execute(const ContextMap &context) const } return false; + case MATCH: { + if (!left.canConvert(QMetaType::QString)) + { + return false; + } + + auto matching = left.toString(); + + switch (right.type()) + { + case QVariant::Type::RegularExpression: { + return right.toRegularExpression() + .match(matching) + .hasMatch(); + } + case QVariant::Type::List: { + auto list = right.toList(); + + // list must be two items + if (list.size() != 2) + return false; + + // list must be a regular expression and an int + if (list.at(0).type() != + QVariant::Type::RegularExpression || + list.at(1).type() != QVariant::Type::Int) + return false; + + auto match = + list.at(0).toRegularExpression().match(matching); + + // if matched, return nth capture group. Otherwise, return false + if (match.hasMatch()) + return match.captured(list.at(1).toInt()); + else + return false; + } + default: + return false; + } + } default: return false; } @@ -383,6 +453,8 @@ QString BinaryOperation::filterString() const return "startswith"; case ENDS_WITH: return "endswith"; + case MATCH: + return "match"; default: return QString(); } diff --git a/src/controllers/filters/parser/Types.hpp b/src/controllers/filters/parser/Types.hpp index 12a2dcfa3..e6a32cf63 100644 --- a/src/controllers/filters/parser/Types.hpp +++ b/src/controllers/filters/parser/Types.hpp @@ -30,6 +30,7 @@ enum TokenType { CONTAINS = 27, STARTS_WITH = 28, ENDS_WITH = 29, + MATCH = 30, BINARY_END = 49, // unary operator @@ -51,6 +52,7 @@ enum TokenType { STRING = 151, INT = 152, IDENTIFIER = 153, + REGULAR_EXPRESSION = 154, NONE = 200 }; @@ -96,6 +98,21 @@ private: TokenType type_; }; +class RegexExpression : public Expression +{ +public: + RegexExpression(QString regex, bool caseInsensitive); + + QVariant execute(const ContextMap &context) const override; + QString debug() const override; + QString filterString() const override; + +private: + QString regexString_; + bool caseInsensitive_; + QRegularExpression regex_; +}; + using ExpressionList = std::vector>; class ListExpression : public Expression