Skip to content

Commit

Permalink
Add regular expression support to filters (Chatterino#2225)
Browse files Browse the repository at this point in the history
  • Loading branch information
dnsge authored Jan 31, 2021
1 parent 278a00a commit 5a29198
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 2 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions src/controllers/filters/parser/FilterParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions src/controllers/filters/parser/Tokenizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/controllers/filters/parser/Tokenizer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions src/controllers/filters/parser/Types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -383,6 +453,8 @@ QString BinaryOperation::filterString() const
return "startswith";
case ENDS_WITH:
return "endswith";
case MATCH:
return "match";
default:
return QString();
}
Expand Down
17 changes: 17 additions & 0 deletions src/controllers/filters/parser/Types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum TokenType {
CONTAINS = 27,
STARTS_WITH = 28,
ENDS_WITH = 29,
MATCH = 30,
BINARY_END = 49,

// unary operator
Expand All @@ -51,6 +52,7 @@ enum TokenType {
STRING = 151,
INT = 152,
IDENTIFIER = 153,
REGULAR_EXPRESSION = 154,

NONE = 200
};
Expand Down Expand Up @@ -96,6 +98,21 @@ class ValueExpression : public Expression
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
Expand Down

0 comments on commit 5a29198

Please sign in to comment.