Add regular expression support to filters (#2225)
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
## Unversioned
|
## 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 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: 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)
|
- 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)
|
- Minor: Added `/marker` command - similar to webchat, it creates a stream marker. (#2360)
|
||||||
|
|||||||
@@ -255,6 +255,16 @@ ExpressionPtr FilterParser::parseValue()
|
|||||||
return std::make_unique<ValueExpression>(this->tokenizer_.next(),
|
return std::make_unique<ValueExpression>(this->tokenizer_.next(),
|
||||||
type);
|
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<RegexExpression>(val, caseInsensitive);
|
||||||
|
}
|
||||||
else if (type == TokenType::LP)
|
else if (type == TokenType::LP)
|
||||||
{
|
{
|
||||||
return this->parseParentheses();
|
return this->parseParentheses();
|
||||||
|
|||||||
@@ -141,10 +141,18 @@ TokenType Tokenizer::tokenize(const QString &text)
|
|||||||
return TokenType::STARTS_WITH;
|
return TokenType::STARTS_WITH;
|
||||||
else if (text == "endswith")
|
else if (text == "endswith")
|
||||||
return TokenType::ENDS_WITH;
|
return TokenType::ENDS_WITH;
|
||||||
|
else if (text == "match")
|
||||||
|
return TokenType::MATCH;
|
||||||
else if (text == "!")
|
else if (text == "!")
|
||||||
return TokenType::NOT;
|
return TokenType::NOT;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if ((text.startsWith("r\"") || text.startsWith("ri\"")) &&
|
||||||
|
text.back() == '"')
|
||||||
|
{
|
||||||
|
return TokenType::REGULAR_EXPRESSION;
|
||||||
|
}
|
||||||
|
|
||||||
if (text.front() == '"' && text.back() == '"')
|
if (text.front() == '"' && text.back() == '"')
|
||||||
return TokenType::STRING;
|
return TokenType::STRING;
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ static const QMap<QString, QString> validIdentifiersMap = {
|
|||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const QRegularExpression tokenRegex(
|
static const QRegularExpression tokenRegex(
|
||||||
QString("\\\"((\\\\\")|[^\\\"])*\\\"|") + // String literal
|
QString("((r|ri)?\\\")((\\\\\")|[^\\\"])*\\\"|") + // String/Regex literal
|
||||||
QString("[\\w\\.]+|") + // Identifier or reserved keyword
|
QString("[\\w\\.]+|") + // Identifier or reserved keyword
|
||||||
QString("(<=?|>=?|!=?|==|\\|\\||&&|\\+|-|\\*|\\/|%)+|") + // Operator
|
QString("(<=?|>=?|!=?|==|\\|\\||&&|\\+|-|\\*|\\/|%)+|") + // Operator
|
||||||
QString("[\\(\\)]|") + // Parentheses
|
QString("[\\(\\)]|") + // Parentheses
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ QString tokenTypeToInfoString(TokenType type)
|
|||||||
return "<starts with>";
|
return "<starts with>";
|
||||||
case ENDS_WITH:
|
case ENDS_WITH:
|
||||||
return "<ends with>";
|
return "<ends with>";
|
||||||
|
case MATCH:
|
||||||
|
return "<match>";
|
||||||
case NOT:
|
case NOT:
|
||||||
return "<not>";
|
return "<not>";
|
||||||
case STRING:
|
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::ListExpression(ExpressionList list)
|
ListExpression::ListExpression(ExpressionList list)
|
||||||
@@ -334,6 +363,47 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -383,6 +453,8 @@ QString BinaryOperation::filterString() const
|
|||||||
return "startswith";
|
return "startswith";
|
||||||
case ENDS_WITH:
|
case ENDS_WITH:
|
||||||
return "endswith";
|
return "endswith";
|
||||||
|
case MATCH:
|
||||||
|
return "match";
|
||||||
default:
|
default:
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ enum TokenType {
|
|||||||
CONTAINS = 27,
|
CONTAINS = 27,
|
||||||
STARTS_WITH = 28,
|
STARTS_WITH = 28,
|
||||||
ENDS_WITH = 29,
|
ENDS_WITH = 29,
|
||||||
|
MATCH = 30,
|
||||||
BINARY_END = 49,
|
BINARY_END = 49,
|
||||||
|
|
||||||
// unary operator
|
// unary operator
|
||||||
@@ -51,6 +52,7 @@ enum TokenType {
|
|||||||
STRING = 151,
|
STRING = 151,
|
||||||
INT = 152,
|
INT = 152,
|
||||||
IDENTIFIER = 153,
|
IDENTIFIER = 153,
|
||||||
|
REGULAR_EXPRESSION = 154,
|
||||||
|
|
||||||
NONE = 200
|
NONE = 200
|
||||||
};
|
};
|
||||||
@@ -96,6 +98,21 @@ private:
|
|||||||
TokenType type_;
|
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<std::unique_ptr<Expression>>;
|
using ExpressionList = std::vector<std::unique_ptr<Expression>>;
|
||||||
|
|
||||||
class ListExpression : public Expression
|
class ListExpression : public Expression
|
||||||
|
|||||||
Reference in New Issue
Block a user