Add regular expression support to filters (#2225)

This commit is contained in:
Daniel 2021-01-31 06:45:15 -05:00 committed by GitHub
parent 278a00a700
commit 5a29198367
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 109 additions and 2 deletions

View file

@ -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)

View file

@ -255,6 +255,16 @@ ExpressionPtr FilterParser::parseValue()
return std::make_unique<ValueExpression>(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<RegexExpression>(val, caseInsensitive);
}
else if (type == TokenType::LP)
{
return this->parseParentheses();

View file

@ -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;

View file

@ -27,7 +27,7 @@ static const QMap<QString, QString> 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

View file

@ -69,6 +69,8 @@ QString tokenTypeToInfoString(TokenType type)
return "<starts with>";
case ENDS_WITH:
return "<ends with>";
case MATCH:
return "<match>";
case NOT:
return "<not>";
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();
}

View file

@ -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<std::unique_ptr<Expression>>;
class ListExpression : public Expression