Implement type checking/validation for filters (#4364)
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
|
- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
|
||||||
- Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463)
|
- Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463)
|
||||||
- Minor: Added the ability to reply to a message by `Shift + Right Click`ing the username. (#4424)
|
- Minor: Added the ability to reply to a message by `Shift + Right Click`ing the username. (#4424)
|
||||||
|
- Minor: Added better filter validation and error messages. (#4364)
|
||||||
- Minor: Updated the look of the Black Theme to be more in line with the other themes. (#4523)
|
- Minor: Updated the look of the Black Theme to be more in line with the other themes. (#4523)
|
||||||
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
|
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
|
||||||
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
|
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
|
||||||
|
|||||||
+20
-6
@@ -74,12 +74,26 @@ set(SOURCE_FILES
|
|||||||
controllers/filters/FilterRecord.hpp
|
controllers/filters/FilterRecord.hpp
|
||||||
controllers/filters/FilterSet.cpp
|
controllers/filters/FilterSet.cpp
|
||||||
controllers/filters/FilterSet.hpp
|
controllers/filters/FilterSet.hpp
|
||||||
controllers/filters/parser/FilterParser.cpp
|
controllers/filters/lang/expressions/Expression.cpp
|
||||||
controllers/filters/parser/FilterParser.hpp
|
controllers/filters/lang/expressions/Expression.hpp
|
||||||
controllers/filters/parser/Tokenizer.cpp
|
controllers/filters/lang/expressions/BinaryOperation.cpp
|
||||||
controllers/filters/parser/Tokenizer.hpp
|
controllers/filters/lang/expressions/BinaryOperation.hpp
|
||||||
controllers/filters/parser/Types.cpp
|
controllers/filters/lang/expressions/ListExpression.cpp
|
||||||
controllers/filters/parser/Types.hpp
|
controllers/filters/lang/expressions/ListExpression.hpp
|
||||||
|
controllers/filters/lang/expressions/RegexExpression.cpp
|
||||||
|
controllers/filters/lang/expressions/RegexExpression.hpp
|
||||||
|
controllers/filters/lang/expressions/UnaryOperation.hpp
|
||||||
|
controllers/filters/lang/expressions/UnaryOperation.cpp
|
||||||
|
controllers/filters/lang/expressions/ValueExpression.cpp
|
||||||
|
controllers/filters/lang/expressions/ValueExpression.hpp
|
||||||
|
controllers/filters/lang/Filter.cpp
|
||||||
|
controllers/filters/lang/Filter.hpp
|
||||||
|
controllers/filters/lang/FilterParser.cpp
|
||||||
|
controllers/filters/lang/FilterParser.hpp
|
||||||
|
controllers/filters/lang/Tokenizer.cpp
|
||||||
|
controllers/filters/lang/Tokenizer.hpp
|
||||||
|
controllers/filters/lang/Types.cpp
|
||||||
|
controllers/filters/lang/Types.hpp
|
||||||
|
|
||||||
controllers/highlights/BadgeHighlightModel.cpp
|
controllers/highlights/BadgeHighlightModel.cpp
|
||||||
controllers/highlights/BadgeHighlightModel.hpp
|
controllers/highlights/BadgeHighlightModel.hpp
|
||||||
|
|||||||
@@ -1,21 +1,40 @@
|
|||||||
#include "controllers/filters/FilterRecord.hpp"
|
#include "controllers/filters/FilterRecord.hpp"
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/Filter.hpp"
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
FilterRecord::FilterRecord(const QString &name, const QString &filter)
|
static std::unique_ptr<filters::Filter> buildFilter(const QString &filterText)
|
||||||
: name_(name)
|
{
|
||||||
, filter_(filter)
|
using namespace filters;
|
||||||
, id_(QUuid::createUuid())
|
auto result = Filter::fromString(filterText);
|
||||||
, parser_(std::make_unique<filterparser::FilterParser>(filter))
|
if (std::holds_alternative<Filter>(result))
|
||||||
|
{
|
||||||
|
auto filter =
|
||||||
|
std::make_unique<Filter>(std::move(std::get<Filter>(result)));
|
||||||
|
|
||||||
|
if (filter->returnType() != Type::Bool)
|
||||||
|
{
|
||||||
|
// Only accept Bool results
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterRecord::FilterRecord(QString name, QString filter)
|
||||||
|
: FilterRecord(std::move(name), std::move(filter), QUuid::createUuid())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterRecord::FilterRecord(const QString &name, const QString &filter,
|
FilterRecord::FilterRecord(QString name, QString filter, const QUuid &id)
|
||||||
const QUuid &id)
|
: name_(std::move(name))
|
||||||
: name_(name)
|
, filterText_(std::move(filter))
|
||||||
, filter_(filter)
|
|
||||||
, id_(id)
|
, id_(id)
|
||||||
, parser_(std::make_unique<filterparser::FilterParser>(filter))
|
, filter_(buildFilter(this->filterText_))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +45,7 @@ const QString &FilterRecord::getName() const
|
|||||||
|
|
||||||
const QString &FilterRecord::getFilter() const
|
const QString &FilterRecord::getFilter() const
|
||||||
{
|
{
|
||||||
return this->filter_;
|
return this->filterText_;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUuid &FilterRecord::getId() const
|
const QUuid &FilterRecord::getId() const
|
||||||
@@ -36,12 +55,13 @@ const QUuid &FilterRecord::getId() const
|
|||||||
|
|
||||||
bool FilterRecord::valid() const
|
bool FilterRecord::valid() const
|
||||||
{
|
{
|
||||||
return this->parser_->valid();
|
return this->filter_ != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FilterRecord::filter(const filterparser::ContextMap &context) const
|
bool FilterRecord::filter(const filters::ContextMap &context) const
|
||||||
{
|
{
|
||||||
return this->parser_->execute(context);
|
assert(this->valid());
|
||||||
|
return this->filter_->execute(context).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FilterRecord::operator==(const FilterRecord &other) const
|
bool FilterRecord::operator==(const FilterRecord &other) const
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "controllers/filters/parser/FilterParser.hpp"
|
#include "controllers/filters/lang/Filter.hpp"
|
||||||
#include "util/RapidjsonHelpers.hpp"
|
#include "util/RapidjsonHelpers.hpp"
|
||||||
#include "util/RapidJsonSerializeQString.hpp"
|
#include "util/RapidJsonSerializeQString.hpp"
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@ namespace chatterino {
|
|||||||
class FilterRecord
|
class FilterRecord
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FilterRecord(const QString &name, const QString &filter);
|
FilterRecord(QString name, QString filter);
|
||||||
|
|
||||||
FilterRecord(const QString &name, const QString &filter, const QUuid &id);
|
FilterRecord(QString name, QString filter, const QUuid &id);
|
||||||
|
|
||||||
const QString &getName() const;
|
const QString &getName() const;
|
||||||
|
|
||||||
@@ -28,16 +28,16 @@ public:
|
|||||||
|
|
||||||
bool valid() const;
|
bool valid() const;
|
||||||
|
|
||||||
bool filter(const filterparser::ContextMap &context) const;
|
bool filter(const filters::ContextMap &context) const;
|
||||||
|
|
||||||
bool operator==(const FilterRecord &other) const;
|
bool operator==(const FilterRecord &other) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString name_;
|
const QString name_;
|
||||||
QString filter_;
|
const QString filterText_;
|
||||||
QUuid id_;
|
const QUuid id_;
|
||||||
|
|
||||||
std::unique_ptr<filterparser::FilterParser> parser_;
|
const std::unique_ptr<filters::Filter> filter_;
|
||||||
};
|
};
|
||||||
|
|
||||||
using FilterRecordPtr = std::shared_ptr<FilterRecord>;
|
using FilterRecordPtr = std::shared_ptr<FilterRecord>;
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ bool FilterSet::filter(const MessagePtr &m, ChannelPtr channel) const
|
|||||||
if (this->filters_.size() == 0)
|
if (this->filters_.size() == 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
filterparser::ContextMap context =
|
filters::ContextMap context = filters::buildContextMap(m, channel.get());
|
||||||
filterparser::buildContextMap(m, channel.get());
|
|
||||||
for (const auto &f : this->filters_.values())
|
for (const auto &f : this->filters_.values())
|
||||||
{
|
{
|
||||||
if (!f->valid() || !f->filter(context))
|
if (!f->valid() || !f->filter(context))
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#include "controllers/filters/lang/Filter.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "common/Channel.hpp"
|
||||||
|
#include "controllers/filters/lang/FilterParser.hpp"
|
||||||
|
#include "messages/Message.hpp"
|
||||||
|
#include "providers/twitch/TwitchBadge.hpp"
|
||||||
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||||
|
{
|
||||||
|
auto watchingChannel = chatterino::getApp()->twitch->watchingChannel.get();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Looking to add a new identifier to filters? Here's what to do:
|
||||||
|
* 1. Update validIdentifiersMap in Tokenizer.hpp
|
||||||
|
* 2. Add the identifier to the list below
|
||||||
|
* 3. Add the type of the identifier to MESSAGE_TYPING_CONTEXT in Filter.hpp
|
||||||
|
* 4. Add the value for the identifier to the ContextMap returned by this function
|
||||||
|
*
|
||||||
|
* List of identifiers:
|
||||||
|
*
|
||||||
|
* author.badges
|
||||||
|
* author.color
|
||||||
|
* author.name
|
||||||
|
* author.no_color
|
||||||
|
* author.subbed
|
||||||
|
* author.sub_length
|
||||||
|
*
|
||||||
|
* channel.name
|
||||||
|
* channel.watching
|
||||||
|
*
|
||||||
|
* flags.highlighted
|
||||||
|
* flags.points_redeemed
|
||||||
|
* flags.sub_message
|
||||||
|
* flags.system_message
|
||||||
|
* flags.reward_message
|
||||||
|
* flags.first_message
|
||||||
|
* flags.elevated_message
|
||||||
|
* flags.cheer_message
|
||||||
|
* flags.whisper
|
||||||
|
* flags.reply
|
||||||
|
* flags.automod
|
||||||
|
*
|
||||||
|
* message.content
|
||||||
|
* message.length
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
using MessageFlag = chatterino::MessageFlag;
|
||||||
|
|
||||||
|
QStringList badges;
|
||||||
|
badges.reserve(m->badges.size());
|
||||||
|
for (const auto &e : m->badges)
|
||||||
|
{
|
||||||
|
badges << e.key_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool watching = !watchingChannel->getName().isEmpty() &&
|
||||||
|
watchingChannel->getName().compare(
|
||||||
|
m->channelName, Qt::CaseInsensitive) == 0;
|
||||||
|
|
||||||
|
bool subscribed = false;
|
||||||
|
int subLength = 0;
|
||||||
|
for (const auto &subBadge : {"subscriber", "founder"})
|
||||||
|
{
|
||||||
|
if (!badges.contains(subBadge))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
subscribed = true;
|
||||||
|
if (m->badgeInfos.find(subBadge) != m->badgeInfos.end())
|
||||||
|
{
|
||||||
|
subLength = m->badgeInfos.at(subBadge).toInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ContextMap vars = {
|
||||||
|
{"author.badges", std::move(badges)},
|
||||||
|
{"author.color", m->usernameColor},
|
||||||
|
{"author.name", m->displayName},
|
||||||
|
{"author.no_color", !m->usernameColor.isValid()},
|
||||||
|
{"author.subbed", subscribed},
|
||||||
|
{"author.sub_length", subLength},
|
||||||
|
|
||||||
|
{"channel.name", m->channelName},
|
||||||
|
{"channel.watching", watching},
|
||||||
|
|
||||||
|
{"flags.highlighted", m->flags.has(MessageFlag::Highlighted)},
|
||||||
|
{"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)},
|
||||||
|
{"flags.sub_message", m->flags.has(MessageFlag::Subscription)},
|
||||||
|
{"flags.system_message", m->flags.has(MessageFlag::System)},
|
||||||
|
{"flags.reward_message",
|
||||||
|
m->flags.has(MessageFlag::RedeemedChannelPointReward)},
|
||||||
|
{"flags.first_message", m->flags.has(MessageFlag::FirstMessage)},
|
||||||
|
{"flags.elevated_message", m->flags.has(MessageFlag::ElevatedMessage)},
|
||||||
|
{"flags.cheer_message", m->flags.has(MessageFlag::CheerMessage)},
|
||||||
|
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
|
||||||
|
{"flags.reply", m->flags.has(MessageFlag::ReplyMessage)},
|
||||||
|
{"flags.automod", m->flags.has(MessageFlag::AutoMod)},
|
||||||
|
|
||||||
|
{"message.content", m->messageText},
|
||||||
|
{"message.length", m->messageText.length()},
|
||||||
|
};
|
||||||
|
{
|
||||||
|
auto *tc = dynamic_cast<TwitchChannel *>(channel);
|
||||||
|
if (channel && !channel->isEmpty() && tc)
|
||||||
|
{
|
||||||
|
vars["channel.live"] = tc->isLive();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
vars["channel.live"] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterResult Filter::fromString(const QString &str)
|
||||||
|
{
|
||||||
|
FilterParser parser(str);
|
||||||
|
|
||||||
|
if (parser.valid())
|
||||||
|
{
|
||||||
|
auto exp = parser.release();
|
||||||
|
auto typ = parser.returnType();
|
||||||
|
return Filter(std::move(exp), typ);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterError{parser.errors().join("\n")};
|
||||||
|
}
|
||||||
|
|
||||||
|
Filter::Filter(ExpressionPtr expression, Type returnType)
|
||||||
|
: expression_(std::move(expression))
|
||||||
|
, returnType_(returnType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Type Filter::returnType() const
|
||||||
|
{
|
||||||
|
return this->returnType_;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant Filter::execute(const ContextMap &context) const
|
||||||
|
{
|
||||||
|
return this->expression_->execute(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Filter::filterString() const
|
||||||
|
{
|
||||||
|
return this->expression_->filterString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Filter::debugString(const TypingContext &context) const
|
||||||
|
{
|
||||||
|
return this->expression_->debug(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class Channel;
|
||||||
|
struct Message;
|
||||||
|
using MessagePtr = std::shared_ptr<const Message>;
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
// MESSAGE_TYPING_CONTEXT maps filter variables to their expected type at evaluation.
|
||||||
|
// For example, flags.highlighted is a boolean variable, so it is marked as Type::Bool
|
||||||
|
// below. These variable types will be used to check whether a filter "makes sense",
|
||||||
|
// i.e. if all the variables and operators being used have compatible types.
|
||||||
|
static const QMap<QString, Type> MESSAGE_TYPING_CONTEXT = {
|
||||||
|
{"author.badges", Type::StringList},
|
||||||
|
{"author.color", Type::Color},
|
||||||
|
{"author.name", Type::String},
|
||||||
|
{"author.no_color", Type::Bool},
|
||||||
|
{"author.subbed", Type::Bool},
|
||||||
|
{"author.sub_length", Type::Int},
|
||||||
|
{"channel.name", Type::String},
|
||||||
|
{"channel.watching", Type::Bool},
|
||||||
|
{"channel.live", Type::Bool},
|
||||||
|
{"flags.highlighted", Type::Bool},
|
||||||
|
{"flags.points_redeemed", Type::Bool},
|
||||||
|
{"flags.sub_message", Type::Bool},
|
||||||
|
{"flags.system_message", Type::Bool},
|
||||||
|
{"flags.reward_message", Type::Bool},
|
||||||
|
{"flags.first_message", Type::Bool},
|
||||||
|
{"flags.elevated_message", Type::Bool},
|
||||||
|
{"flags.cheer_message", Type::Bool},
|
||||||
|
{"flags.whisper", Type::Bool},
|
||||||
|
{"flags.reply", Type::Bool},
|
||||||
|
{"flags.automod", Type::Bool},
|
||||||
|
{"message.content", Type::String},
|
||||||
|
{"message.length", Type::Int},
|
||||||
|
};
|
||||||
|
|
||||||
|
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel);
|
||||||
|
|
||||||
|
class Filter;
|
||||||
|
struct FilterError {
|
||||||
|
QString message;
|
||||||
|
};
|
||||||
|
|
||||||
|
using FilterResult = std::variant<Filter, FilterError>;
|
||||||
|
|
||||||
|
class Filter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static FilterResult fromString(const QString &str);
|
||||||
|
|
||||||
|
Type returnType() const;
|
||||||
|
QVariant execute(const ContextMap &context) const;
|
||||||
|
|
||||||
|
QString filterString() const;
|
||||||
|
QString debugString(const TypingContext &context) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Filter(ExpressionPtr expression, Type returnType);
|
||||||
|
|
||||||
|
ExpressionPtr expression_;
|
||||||
|
Type returnType_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
+43
-119
@@ -1,115 +1,21 @@
|
|||||||
#include "FilterParser.hpp"
|
#include "controllers/filters/lang/FilterParser.hpp"
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "controllers/filters/lang/expressions/BinaryOperation.hpp"
|
||||||
#include "common/Channel.hpp"
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
#include "controllers/filters/parser/Types.hpp"
|
#include "controllers/filters/lang/expressions/ListExpression.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "controllers/filters/lang/expressions/RegexExpression.hpp"
|
||||||
#include "providers/twitch/TwitchBadge.hpp"
|
#include "controllers/filters/lang/expressions/UnaryOperation.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "controllers/filters/lang/expressions/ValueExpression.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "controllers/filters/lang/Filter.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
namespace filterparser {
|
namespace chatterino::filters {
|
||||||
|
|
||||||
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
QString explainIllType(const IllTyped &ill)
|
||||||
{
|
{
|
||||||
auto watchingChannel = chatterino::getApp()->twitch->watchingChannel.get();
|
return QString("%1\n\nProblem occurred here:\n%2")
|
||||||
|
.arg(ill.message)
|
||||||
/* Known Identifiers
|
.arg(ill.expr->filterString());
|
||||||
*
|
|
||||||
* author.badges
|
|
||||||
* author.color
|
|
||||||
* author.name
|
|
||||||
* author.no_color
|
|
||||||
* author.subbed
|
|
||||||
* author.sub_length
|
|
||||||
*
|
|
||||||
* channel.name
|
|
||||||
* channel.watching
|
|
||||||
*
|
|
||||||
* flags.highlighted
|
|
||||||
* flags.points_redeemed
|
|
||||||
* flags.sub_message
|
|
||||||
* flags.system_message
|
|
||||||
* flags.reward_message
|
|
||||||
* flags.first_message
|
|
||||||
* flags.elevated_message
|
|
||||||
* flags.cheer_message
|
|
||||||
* flags.whisper
|
|
||||||
* flags.reply
|
|
||||||
* flags.automod
|
|
||||||
*
|
|
||||||
* message.content
|
|
||||||
* message.length
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
using MessageFlag = chatterino::MessageFlag;
|
|
||||||
|
|
||||||
QStringList badges;
|
|
||||||
badges.reserve(m->badges.size());
|
|
||||||
for (const auto &e : m->badges)
|
|
||||||
{
|
|
||||||
badges << e.key_;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool watching = !watchingChannel->getName().isEmpty() &&
|
|
||||||
watchingChannel->getName().compare(
|
|
||||||
m->channelName, Qt::CaseInsensitive) == 0;
|
|
||||||
|
|
||||||
bool subscribed = false;
|
|
||||||
int subLength = 0;
|
|
||||||
for (const auto &subBadge : {"subscriber", "founder"})
|
|
||||||
{
|
|
||||||
if (!badges.contains(subBadge))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
subscribed = true;
|
|
||||||
if (m->badgeInfos.find(subBadge) != m->badgeInfos.end())
|
|
||||||
{
|
|
||||||
subLength = m->badgeInfos.at(subBadge).toInt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ContextMap vars = {
|
|
||||||
{"author.badges", std::move(badges)},
|
|
||||||
{"author.color", m->usernameColor},
|
|
||||||
{"author.name", m->displayName},
|
|
||||||
{"author.no_color", !m->usernameColor.isValid()},
|
|
||||||
{"author.subbed", subscribed},
|
|
||||||
{"author.sub_length", subLength},
|
|
||||||
|
|
||||||
{"channel.name", m->channelName},
|
|
||||||
{"channel.watching", watching},
|
|
||||||
|
|
||||||
{"flags.highlighted", m->flags.has(MessageFlag::Highlighted)},
|
|
||||||
{"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)},
|
|
||||||
{"flags.sub_message", m->flags.has(MessageFlag::Subscription)},
|
|
||||||
{"flags.system_message", m->flags.has(MessageFlag::System)},
|
|
||||||
{"flags.reward_message",
|
|
||||||
m->flags.has(MessageFlag::RedeemedChannelPointReward)},
|
|
||||||
{"flags.first_message", m->flags.has(MessageFlag::FirstMessage)},
|
|
||||||
{"flags.elevated_message", m->flags.has(MessageFlag::ElevatedMessage)},
|
|
||||||
{"flags.cheer_message", m->flags.has(MessageFlag::CheerMessage)},
|
|
||||||
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
|
|
||||||
{"flags.reply", m->flags.has(MessageFlag::ReplyMessage)},
|
|
||||||
{"flags.automod", m->flags.has(MessageFlag::AutoMod)},
|
|
||||||
|
|
||||||
{"message.content", m->messageText},
|
|
||||||
{"message.length", m->messageText.length()},
|
|
||||||
};
|
|
||||||
{
|
|
||||||
using namespace chatterino;
|
|
||||||
auto *tc = dynamic_cast<TwitchChannel *>(channel);
|
|
||||||
if (channel && !channel->isEmpty() && tc)
|
|
||||||
{
|
|
||||||
vars["channel.live"] = tc->isLive();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
vars["channel.live"] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterParser::FilterParser(const QString &text)
|
FilterParser::FilterParser(const QString &text)
|
||||||
@@ -117,11 +23,22 @@ FilterParser::FilterParser(const QString &text)
|
|||||||
, tokenizer_(Tokenizer(text))
|
, tokenizer_(Tokenizer(text))
|
||||||
, builtExpression_(this->parseExpression(true))
|
, builtExpression_(this->parseExpression(true))
|
||||||
{
|
{
|
||||||
}
|
if (!this->valid_)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool FilterParser::execute(const ContextMap &context) const
|
// safety: returnType must not live longer than the parsed expression. See
|
||||||
{
|
// comment on IllTyped::expr.
|
||||||
return this->builtExpression_->execute(context).toBool();
|
auto returnType =
|
||||||
|
this->builtExpression_->synthesizeType(MESSAGE_TYPING_CONTEXT);
|
||||||
|
if (isIllTyped(returnType))
|
||||||
|
{
|
||||||
|
this->errorLog(explainIllType(std::get<IllTyped>(returnType)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->returnType_ = std::get<TypeClass>(returnType).type;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FilterParser::valid() const
|
bool FilterParser::valid() const
|
||||||
@@ -129,6 +46,18 @@ bool FilterParser::valid() const
|
|||||||
return this->valid_;
|
return this->valid_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Type FilterParser::returnType() const
|
||||||
|
{
|
||||||
|
return this->returnType_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpressionPtr FilterParser::release()
|
||||||
|
{
|
||||||
|
ExpressionPtr ret;
|
||||||
|
this->builtExpression_.swap(ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
ExpressionPtr FilterParser::parseExpression(bool top)
|
ExpressionPtr FilterParser::parseExpression(bool top)
|
||||||
{
|
{
|
||||||
auto e = this->parseAnd();
|
auto e = this->parseAnd();
|
||||||
@@ -379,12 +308,7 @@ const QStringList &FilterParser::errors() const
|
|||||||
|
|
||||||
const QString FilterParser::debugString() const
|
const QString FilterParser::debugString() const
|
||||||
{
|
{
|
||||||
return this->builtExpression_->debug();
|
return this->builtExpression_->debug(MESSAGE_TYPING_CONTEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString FilterParser::filterString() const
|
} // namespace chatterino::filters
|
||||||
{
|
|
||||||
return this->builtExpression_->filterString();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace filterparser
|
|
||||||
+10
-14
@@ -1,28 +1,22 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "controllers/filters/parser/Tokenizer.hpp"
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
#include "controllers/filters/parser/Types.hpp"
|
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino::filters {
|
||||||
|
|
||||||
class Channel;
|
|
||||||
|
|
||||||
} // namespace chatterino
|
|
||||||
|
|
||||||
namespace filterparser {
|
|
||||||
|
|
||||||
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel);
|
|
||||||
|
|
||||||
class FilterParser
|
class FilterParser
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FilterParser(const QString &text);
|
FilterParser(const QString &text);
|
||||||
bool execute(const ContextMap &context) const;
|
|
||||||
bool valid() const;
|
bool valid() const;
|
||||||
|
Type returnType() const;
|
||||||
|
ExpressionPtr release();
|
||||||
|
|
||||||
const QStringList &errors() const;
|
const QStringList &errors() const;
|
||||||
const QString debugString() const;
|
const QString debugString() const;
|
||||||
const QString filterString() const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ExpressionPtr parseExpression(bool top = false);
|
ExpressionPtr parseExpression(bool top = false);
|
||||||
@@ -41,5 +35,7 @@ private:
|
|||||||
QString text_;
|
QString text_;
|
||||||
Tokenizer tokenizer_;
|
Tokenizer tokenizer_;
|
||||||
ExpressionPtr builtExpression_;
|
ExpressionPtr builtExpression_;
|
||||||
|
Type returnType_ = Type::Bool;
|
||||||
};
|
};
|
||||||
} // namespace filterparser
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
+74
-3
@@ -1,8 +1,79 @@
|
|||||||
#include "controllers/filters/parser/Tokenizer.hpp"
|
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||||
|
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
|
|
||||||
namespace filterparser {
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
QString tokenTypeToInfoString(TokenType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case AND:
|
||||||
|
return "And";
|
||||||
|
case OR:
|
||||||
|
return "Or";
|
||||||
|
case LP:
|
||||||
|
return "<left parenthesis>";
|
||||||
|
case RP:
|
||||||
|
return "<right parenthesis>";
|
||||||
|
case LIST_START:
|
||||||
|
return "<list start>";
|
||||||
|
case LIST_END:
|
||||||
|
return "<list end>";
|
||||||
|
case COMMA:
|
||||||
|
return "<comma>";
|
||||||
|
case PLUS:
|
||||||
|
return "Plus";
|
||||||
|
case MINUS:
|
||||||
|
return "Minus";
|
||||||
|
case MULTIPLY:
|
||||||
|
return "Multiply";
|
||||||
|
case DIVIDE:
|
||||||
|
return "Divide";
|
||||||
|
case MOD:
|
||||||
|
return "Mod";
|
||||||
|
case EQ:
|
||||||
|
return "Eq";
|
||||||
|
case NEQ:
|
||||||
|
return "NotEq";
|
||||||
|
case LT:
|
||||||
|
return "LessThan";
|
||||||
|
case GT:
|
||||||
|
return "GreaterThan";
|
||||||
|
case LTE:
|
||||||
|
return "LessThanEq";
|
||||||
|
case GTE:
|
||||||
|
return "GreaterThanEq";
|
||||||
|
case CONTAINS:
|
||||||
|
return "Contains";
|
||||||
|
case STARTS_WITH:
|
||||||
|
return "StartsWith";
|
||||||
|
case ENDS_WITH:
|
||||||
|
return "EndsWith";
|
||||||
|
case MATCH:
|
||||||
|
return "Match";
|
||||||
|
case NOT:
|
||||||
|
return "Not";
|
||||||
|
case STRING:
|
||||||
|
return "<string>";
|
||||||
|
case INT:
|
||||||
|
return "<int>";
|
||||||
|
case IDENTIFIER:
|
||||||
|
return "<identifier>";
|
||||||
|
case CONTROL_START:
|
||||||
|
case CONTROL_END:
|
||||||
|
case BINARY_START:
|
||||||
|
case BINARY_END:
|
||||||
|
case UNARY_START:
|
||||||
|
case UNARY_END:
|
||||||
|
case MATH_START:
|
||||||
|
case MATH_END:
|
||||||
|
case OTHER_START:
|
||||||
|
case NONE:
|
||||||
|
default:
|
||||||
|
return "<unknown>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Tokenizer::Tokenizer(const QString &text)
|
Tokenizer::Tokenizer(const QString &text)
|
||||||
{
|
{
|
||||||
@@ -190,4 +261,4 @@ bool Tokenizer::typeIsMathOp(TokenType token)
|
|||||||
return token > TokenType::MATH_START && token < TokenType::MATH_END;
|
return token > TokenType::MATH_START && token < TokenType::MATH_END;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace filterparser
|
} // namespace chatterino::filters
|
||||||
+56
-4
@@ -1,12 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "controllers/filters/parser/Types.hpp"
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
namespace filterparser {
|
namespace chatterino::filters {
|
||||||
|
|
||||||
static const QMap<QString, QString> validIdentifiersMap = {
|
static const QMap<QString, QString> validIdentifiersMap = {
|
||||||
{"author.badges", "author badges"},
|
{"author.badges", "author badges"},
|
||||||
@@ -17,7 +17,7 @@ static const QMap<QString, QString> validIdentifiersMap = {
|
|||||||
{"author.sub_length", "author sub length"},
|
{"author.sub_length", "author sub length"},
|
||||||
{"channel.name", "channel name"},
|
{"channel.name", "channel name"},
|
||||||
{"channel.watching", "/watching channel?"},
|
{"channel.watching", "/watching channel?"},
|
||||||
{"channel.live", "Channel live?"},
|
{"channel.live", "channel live?"},
|
||||||
{"flags.highlighted", "highlighted?"},
|
{"flags.highlighted", "highlighted?"},
|
||||||
{"flags.points_redeemed", "redeemed points?"},
|
{"flags.points_redeemed", "redeemed points?"},
|
||||||
{"flags.sub_message", "sub/resub message?"},
|
{"flags.sub_message", "sub/resub message?"},
|
||||||
@@ -42,6 +42,58 @@ static const QRegularExpression tokenRegex(
|
|||||||
);
|
);
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
|
enum TokenType {
|
||||||
|
// control
|
||||||
|
CONTROL_START = 0,
|
||||||
|
AND = 1,
|
||||||
|
OR = 2,
|
||||||
|
LP = 3,
|
||||||
|
RP = 4,
|
||||||
|
LIST_START = 5,
|
||||||
|
LIST_END = 6,
|
||||||
|
COMMA = 7,
|
||||||
|
CONTROL_END = 19,
|
||||||
|
|
||||||
|
// binary operator
|
||||||
|
BINARY_START = 20,
|
||||||
|
EQ = 21,
|
||||||
|
NEQ = 22,
|
||||||
|
LT = 23,
|
||||||
|
GT = 24,
|
||||||
|
LTE = 25,
|
||||||
|
GTE = 26,
|
||||||
|
CONTAINS = 27,
|
||||||
|
STARTS_WITH = 28,
|
||||||
|
ENDS_WITH = 29,
|
||||||
|
MATCH = 30,
|
||||||
|
BINARY_END = 49,
|
||||||
|
|
||||||
|
// unary operator
|
||||||
|
UNARY_START = 50,
|
||||||
|
NOT = 51,
|
||||||
|
UNARY_END = 99,
|
||||||
|
|
||||||
|
// math operators
|
||||||
|
MATH_START = 100,
|
||||||
|
PLUS = 101,
|
||||||
|
MINUS = 102,
|
||||||
|
MULTIPLY = 103,
|
||||||
|
DIVIDE = 104,
|
||||||
|
MOD = 105,
|
||||||
|
MATH_END = 149,
|
||||||
|
|
||||||
|
// other types
|
||||||
|
OTHER_START = 150,
|
||||||
|
STRING = 151,
|
||||||
|
INT = 152,
|
||||||
|
IDENTIFIER = 153,
|
||||||
|
REGULAR_EXPRESSION = 154,
|
||||||
|
|
||||||
|
NONE = 200
|
||||||
|
};
|
||||||
|
|
||||||
|
QString tokenTypeToInfoString(TokenType type);
|
||||||
|
|
||||||
class Tokenizer
|
class Tokenizer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -74,4 +126,4 @@ private:
|
|||||||
|
|
||||||
TokenType tokenize(const QString &text);
|
TokenType tokenize(const QString &text);
|
||||||
};
|
};
|
||||||
} // namespace filterparser
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
|
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
bool isList(const PossibleType &possibleType)
|
||||||
|
{
|
||||||
|
using T = Type;
|
||||||
|
if (isIllTyped(possibleType))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto typ = std::get<TypeClass>(possibleType);
|
||||||
|
return typ == T::List || typ == T::StringList ||
|
||||||
|
typ == T::MatchingSpecifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString typeToString(Type type)
|
||||||
|
{
|
||||||
|
using T = Type;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case T::String:
|
||||||
|
return "String";
|
||||||
|
case T::Int:
|
||||||
|
return "Int";
|
||||||
|
case T::Bool:
|
||||||
|
return "Bool";
|
||||||
|
case T::Color:
|
||||||
|
return "Color";
|
||||||
|
case T::RegularExpression:
|
||||||
|
return "RegularExpression";
|
||||||
|
case T::List:
|
||||||
|
return "List";
|
||||||
|
case T::StringList:
|
||||||
|
return "StringList";
|
||||||
|
case T::MatchingSpecifier:
|
||||||
|
return "MatchingSpecifier";
|
||||||
|
case T::Map:
|
||||||
|
return "Map";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TypeClass::string() const
|
||||||
|
{
|
||||||
|
return typeToString(this->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TypeClass::operator==(Type t) const
|
||||||
|
{
|
||||||
|
return this->type == t;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TypeClass::operator==(const TypeClass &t) const
|
||||||
|
{
|
||||||
|
return this->type == t.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TypeClass::operator==(const IllTyped &t) const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TypeClass::operator!=(Type t) const
|
||||||
|
{
|
||||||
|
return !this->operator==(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TypeClass::operator!=(const TypeClass &t) const
|
||||||
|
{
|
||||||
|
return !this->operator==(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TypeClass::operator!=(const IllTyped &t) const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString IllTyped::string() const
|
||||||
|
{
|
||||||
|
return "IllTyped";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString possibleTypeToString(const PossibleType &possible)
|
||||||
|
{
|
||||||
|
if (isWellTyped(possible))
|
||||||
|
{
|
||||||
|
return std::get<TypeClass>(possible).string();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return std::get<IllTyped>(possible).string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QMap>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
class Expression;
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
String,
|
||||||
|
Int,
|
||||||
|
Bool,
|
||||||
|
Color,
|
||||||
|
RegularExpression,
|
||||||
|
List,
|
||||||
|
StringList, // List of only strings
|
||||||
|
MatchingSpecifier, // 2-element list in {RegularExpression, Int} form
|
||||||
|
Map
|
||||||
|
};
|
||||||
|
|
||||||
|
using ContextMap = QMap<QString, QVariant>;
|
||||||
|
using TypingContext = QMap<QString, Type>;
|
||||||
|
|
||||||
|
QString typeToString(Type type);
|
||||||
|
|
||||||
|
struct IllTyped;
|
||||||
|
|
||||||
|
struct TypeClass {
|
||||||
|
Type type;
|
||||||
|
|
||||||
|
QString string() const;
|
||||||
|
|
||||||
|
bool operator==(Type t) const;
|
||||||
|
bool operator==(const TypeClass &t) const;
|
||||||
|
bool operator==(const IllTyped &t) const;
|
||||||
|
bool operator!=(Type t) const;
|
||||||
|
bool operator!=(const TypeClass &t) const;
|
||||||
|
bool operator!=(const IllTyped &t) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IllTyped {
|
||||||
|
// Important nuance to expr:
|
||||||
|
// During type synthesis, should an error occur and an IllTyped PossibleType be
|
||||||
|
// returned, expr is a pointer to an Expression that exists in the Expression
|
||||||
|
// tree that was parsed. Therefore, you cannot hold on to this pointer longer
|
||||||
|
// than the Expression tree exists. Be careful!
|
||||||
|
const Expression *expr;
|
||||||
|
QString message;
|
||||||
|
|
||||||
|
QString string() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
using PossibleType = std::variant<TypeClass, IllTyped>;
|
||||||
|
|
||||||
|
inline bool isWellTyped(const PossibleType &possible)
|
||||||
|
{
|
||||||
|
return std::holds_alternative<TypeClass>(possible);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool isIllTyped(const PossibleType &possible)
|
||||||
|
{
|
||||||
|
return std::holds_alternative<IllTyped>(possible);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString possibleTypeToString(const PossibleType &possible);
|
||||||
|
|
||||||
|
bool isList(const PossibleType &possibleType);
|
||||||
|
|
||||||
|
inline bool variantIs(const QVariant &a, QMetaType::Type type)
|
||||||
|
{
|
||||||
|
return static_cast<QMetaType::Type>(a.type()) == type;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool variantIsNot(const QVariant &a, QMetaType::Type type)
|
||||||
|
{
|
||||||
|
return static_cast<QMetaType::Type>(a.type()) != type;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool convertVariantTypes(QVariant &a, QVariant &b, int type)
|
||||||
|
{
|
||||||
|
return a.convert(type) && b.convert(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool variantTypesMatch(QVariant &a, QVariant &b, QMetaType::Type type)
|
||||||
|
{
|
||||||
|
return variantIs(a, type) && variantIs(b, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
+125
-279
@@ -1,214 +1,8 @@
|
|||||||
#include "controllers/filters/parser/Types.hpp"
|
#include "controllers/filters/lang/expressions/BinaryOperation.hpp"
|
||||||
|
|
||||||
namespace filterparser {
|
#include <QRegularExpression>
|
||||||
|
|
||||||
bool convertVariantTypes(QVariant &a, QVariant &b, int type)
|
namespace chatterino::filters {
|
||||||
{
|
|
||||||
return a.convert(type) && b.convert(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool variantTypesMatch(QVariant &a, QVariant &b, QVariant::Type type)
|
|
||||||
{
|
|
||||||
return a.type() == type && b.type() == type;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString tokenTypeToInfoString(TokenType type)
|
|
||||||
{
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case CONTROL_START:
|
|
||||||
case CONTROL_END:
|
|
||||||
case BINARY_START:
|
|
||||||
case BINARY_END:
|
|
||||||
case UNARY_START:
|
|
||||||
case UNARY_END:
|
|
||||||
case MATH_START:
|
|
||||||
case MATH_END:
|
|
||||||
case OTHER_START:
|
|
||||||
case NONE:
|
|
||||||
return "<unknown>";
|
|
||||||
case AND:
|
|
||||||
return "<and>";
|
|
||||||
case OR:
|
|
||||||
return "<or>";
|
|
||||||
case LP:
|
|
||||||
return "<left parenthesis>";
|
|
||||||
case RP:
|
|
||||||
return "<right parenthesis>";
|
|
||||||
case LIST_START:
|
|
||||||
return "<list start>";
|
|
||||||
case LIST_END:
|
|
||||||
return "<list end>";
|
|
||||||
case COMMA:
|
|
||||||
return "<comma>";
|
|
||||||
case PLUS:
|
|
||||||
return "<plus>";
|
|
||||||
case MINUS:
|
|
||||||
return "<minus>";
|
|
||||||
case MULTIPLY:
|
|
||||||
return "<multiply>";
|
|
||||||
case DIVIDE:
|
|
||||||
return "<divide>";
|
|
||||||
case MOD:
|
|
||||||
return "<modulus>";
|
|
||||||
case EQ:
|
|
||||||
return "<equals>";
|
|
||||||
case NEQ:
|
|
||||||
return "<not equals>";
|
|
||||||
case LT:
|
|
||||||
return "<less than>";
|
|
||||||
case GT:
|
|
||||||
return "<greater than>";
|
|
||||||
case LTE:
|
|
||||||
return "<less than equal>";
|
|
||||||
case GTE:
|
|
||||||
return "<greater than equal>";
|
|
||||||
case CONTAINS:
|
|
||||||
return "<contains>";
|
|
||||||
case STARTS_WITH:
|
|
||||||
return "<starts with>";
|
|
||||||
case ENDS_WITH:
|
|
||||||
return "<ends with>";
|
|
||||||
case MATCH:
|
|
||||||
return "<match>";
|
|
||||||
case NOT:
|
|
||||||
return "<not>";
|
|
||||||
case STRING:
|
|
||||||
return "<string>";
|
|
||||||
case INT:
|
|
||||||
return "<int>";
|
|
||||||
case IDENTIFIER:
|
|
||||||
return "<identifier>";
|
|
||||||
default:
|
|
||||||
return "<unknown>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValueExpression
|
|
||||||
|
|
||||||
ValueExpression::ValueExpression(QVariant value, TokenType type)
|
|
||||||
: value_(value)
|
|
||||||
, type_(type){};
|
|
||||||
|
|
||||||
QVariant ValueExpression::execute(const ContextMap &context) const
|
|
||||||
{
|
|
||||||
if (this->type_ == TokenType::IDENTIFIER)
|
|
||||||
{
|
|
||||||
return context.value(this->value_.toString());
|
|
||||||
}
|
|
||||||
return this->value_;
|
|
||||||
}
|
|
||||||
|
|
||||||
TokenType ValueExpression::type()
|
|
||||||
{
|
|
||||||
return this->type_;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ValueExpression::debug() const
|
|
||||||
{
|
|
||||||
return this->value_.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ValueExpression::filterString() const
|
|
||||||
{
|
|
||||||
switch (this->type_)
|
|
||||||
{
|
|
||||||
case INT:
|
|
||||||
return QString::number(this->value_.toInt());
|
|
||||||
case STRING:
|
|
||||||
return QString("\"%1\"").arg(
|
|
||||||
this->value_.toString().replace("\"", "\\\""));
|
|
||||||
case IDENTIFIER:
|
|
||||||
return this->value_.toString();
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
: list_(std::move(list)){};
|
|
||||||
|
|
||||||
QVariant ListExpression::execute(const ContextMap &context) const
|
|
||||||
{
|
|
||||||
QList<QVariant> results;
|
|
||||||
bool allStrings = true;
|
|
||||||
for (const auto &exp : this->list_)
|
|
||||||
{
|
|
||||||
auto res = exp->execute(context);
|
|
||||||
if (allStrings && res.type() != QVariant::Type::String)
|
|
||||||
{
|
|
||||||
allStrings = false;
|
|
||||||
}
|
|
||||||
results.append(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if everything is a string return a QStringList for case-insensitive comparison
|
|
||||||
if (allStrings)
|
|
||||||
{
|
|
||||||
QStringList strings;
|
|
||||||
strings.reserve(results.size());
|
|
||||||
for (const auto &val : results)
|
|
||||||
{
|
|
||||||
strings << val.toString();
|
|
||||||
}
|
|
||||||
return strings;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ListExpression::debug() const
|
|
||||||
{
|
|
||||||
QStringList debugs;
|
|
||||||
for (const auto &exp : this->list_)
|
|
||||||
{
|
|
||||||
debugs.append(exp->debug());
|
|
||||||
}
|
|
||||||
return QString("{%1}").arg(debugs.join(", "));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ListExpression::filterString() const
|
|
||||||
{
|
|
||||||
QStringList strings;
|
|
||||||
for (const auto &exp : this->list_)
|
|
||||||
{
|
|
||||||
strings.append(QString("(%1)").arg(exp->filterString()));
|
|
||||||
}
|
|
||||||
return QString("{%1}").arg(strings.join(", "));
|
|
||||||
}
|
|
||||||
|
|
||||||
// BinaryOperation
|
|
||||||
|
|
||||||
BinaryOperation::BinaryOperation(TokenType op, ExpressionPtr left,
|
BinaryOperation::BinaryOperation(TokenType op, ExpressionPtr left,
|
||||||
ExpressionPtr right)
|
ExpressionPtr right)
|
||||||
@@ -225,7 +19,8 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||||||
switch (this->op_)
|
switch (this->op_)
|
||||||
{
|
{
|
||||||
case PLUS:
|
case PLUS:
|
||||||
if (left.type() == QVariant::Type::String &&
|
if (static_cast<QMetaType::Type>(left.type()) ==
|
||||||
|
QMetaType::QString &&
|
||||||
right.canConvert(QMetaType::QString))
|
right.canConvert(QMetaType::QString))
|
||||||
{
|
{
|
||||||
return left.toString().append(right.toString());
|
return left.toString().append(right.toString());
|
||||||
@@ -260,14 +55,14 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||||||
return left.toBool() && right.toBool();
|
return left.toBool() && right.toBool();
|
||||||
return false;
|
return false;
|
||||||
case EQ:
|
case EQ:
|
||||||
if (variantTypesMatch(left, right, QVariant::Type::String))
|
if (variantTypesMatch(left, right, QMetaType::QString))
|
||||||
{
|
{
|
||||||
return left.toString().compare(right.toString(),
|
return left.toString().compare(right.toString(),
|
||||||
Qt::CaseInsensitive) == 0;
|
Qt::CaseInsensitive) == 0;
|
||||||
}
|
}
|
||||||
return left == right;
|
return left == right;
|
||||||
case NEQ:
|
case NEQ:
|
||||||
if (variantTypesMatch(left, right, QVariant::Type::String))
|
if (variantTypesMatch(left, right, QMetaType::QString))
|
||||||
{
|
{
|
||||||
return left.toString().compare(right.toString(),
|
return left.toString().compare(right.toString(),
|
||||||
Qt::CaseInsensitive) != 0;
|
Qt::CaseInsensitive) != 0;
|
||||||
@@ -290,20 +85,20 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||||||
return left.toInt() >= right.toInt();
|
return left.toInt() >= right.toInt();
|
||||||
return false;
|
return false;
|
||||||
case CONTAINS:
|
case CONTAINS:
|
||||||
if (left.type() == QVariant::Type::StringList &&
|
if (variantIs(left, QMetaType::QStringList) &&
|
||||||
right.canConvert(QMetaType::QString))
|
right.canConvert(QMetaType::QString))
|
||||||
{
|
{
|
||||||
return left.toStringList().contains(right.toString(),
|
return left.toStringList().contains(right.toString(),
|
||||||
Qt::CaseInsensitive);
|
Qt::CaseInsensitive);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (left.type() == QVariant::Type::Map &&
|
if (variantIs(left.type(), QMetaType::QVariantMap) &&
|
||||||
right.canConvert(QMetaType::QString))
|
right.canConvert(QMetaType::QString))
|
||||||
{
|
{
|
||||||
return left.toMap().contains(right.toString());
|
return left.toMap().contains(right.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (left.type() == QVariant::Type::List)
|
if (variantIs(left.type(), QMetaType::QVariantList))
|
||||||
{
|
{
|
||||||
return left.toList().contains(right);
|
return left.toList().contains(right);
|
||||||
}
|
}
|
||||||
@@ -317,16 +112,16 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
case STARTS_WITH:
|
case STARTS_WITH:
|
||||||
if (left.type() == QVariant::Type::StringList &&
|
if (variantIs(left.type(), QMetaType::QStringList) &&
|
||||||
right.canConvert(QMetaType::QString))
|
right.canConvert(QMetaType::QString))
|
||||||
{
|
{
|
||||||
auto list = left.toStringList();
|
auto list = left.toStringList();
|
||||||
return !list.isEmpty() &&
|
return !list.isEmpty() &&
|
||||||
list.first().compare(right.toString(),
|
list.first().compare(right.toString(),
|
||||||
Qt::CaseInsensitive);
|
Qt::CaseInsensitive) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (left.type() == QVariant::Type::List)
|
if (variantIs(left.type(), QMetaType::QVariantList))
|
||||||
{
|
{
|
||||||
return left.toList().startsWith(right);
|
return left.toList().startsWith(right);
|
||||||
}
|
}
|
||||||
@@ -341,16 +136,16 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
case ENDS_WITH:
|
case ENDS_WITH:
|
||||||
if (left.type() == QVariant::Type::StringList &&
|
if (variantIs(left.type(), QMetaType::QStringList) &&
|
||||||
right.canConvert(QMetaType::QString))
|
right.canConvert(QMetaType::QString))
|
||||||
{
|
{
|
||||||
auto list = left.toStringList();
|
auto list = left.toStringList();
|
||||||
return !list.isEmpty() &&
|
return !list.isEmpty() &&
|
||||||
list.last().compare(right.toString(),
|
list.last().compare(right.toString(),
|
||||||
Qt::CaseInsensitive);
|
Qt::CaseInsensitive) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (left.type() == QVariant::Type::List)
|
if (variantIs(left.type(), QMetaType::QVariantList))
|
||||||
{
|
{
|
||||||
return left.toList().endsWith(right);
|
return left.toList().endsWith(right);
|
||||||
}
|
}
|
||||||
@@ -371,14 +166,14 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||||||
|
|
||||||
auto matching = left.toString();
|
auto matching = left.toString();
|
||||||
|
|
||||||
switch (right.type())
|
switch (static_cast<QMetaType::Type>(right.type()))
|
||||||
{
|
{
|
||||||
case QVariant::Type::RegularExpression: {
|
case QMetaType::QRegularExpression: {
|
||||||
return right.toRegularExpression()
|
return right.toRegularExpression()
|
||||||
.match(matching)
|
.match(matching)
|
||||||
.hasMatch();
|
.hasMatch();
|
||||||
}
|
}
|
||||||
case QVariant::Type::List: {
|
case QMetaType::QVariantList: {
|
||||||
auto list = right.toList();
|
auto list = right.toList();
|
||||||
|
|
||||||
// list must be two items
|
// list must be two items
|
||||||
@@ -386,19 +181,19 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
// list must be a regular expression and an int
|
// list must be a regular expression and an int
|
||||||
if (list.at(0).type() !=
|
if (variantIsNot(list.at(0),
|
||||||
QVariant::Type::RegularExpression ||
|
QMetaType::QRegularExpression) ||
|
||||||
list.at(1).type() != QVariant::Type::Int)
|
variantIsNot(list.at(1), QMetaType::Int))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
auto match =
|
auto match =
|
||||||
list.at(0).toRegularExpression().match(matching);
|
list.at(0).toRegularExpression().match(matching);
|
||||||
|
|
||||||
// if matched, return nth capture group. Otherwise, return false
|
// if matched, return nth capture group. Otherwise, return ""
|
||||||
if (match.hasMatch())
|
if (match.hasMatch())
|
||||||
return match.captured(list.at(1).toInt());
|
return match.captured(list.at(1).toInt());
|
||||||
else
|
else
|
||||||
return false;
|
return "";
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -409,11 +204,105 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString BinaryOperation::debug() const
|
PossibleType BinaryOperation::synthesizeType(const TypingContext &context) const
|
||||||
{
|
{
|
||||||
return QString("(%1 %2 %3)")
|
auto leftSyn = this->left_->synthesizeType(context);
|
||||||
.arg(this->left_->debug(), tokenTypeToInfoString(this->op_),
|
auto rightSyn = this->right_->synthesizeType(context);
|
||||||
this->right_->debug());
|
|
||||||
|
// Return if either operand is ill-typed
|
||||||
|
if (isIllTyped(leftSyn))
|
||||||
|
{
|
||||||
|
return leftSyn;
|
||||||
|
}
|
||||||
|
else if (isIllTyped(rightSyn))
|
||||||
|
{
|
||||||
|
return rightSyn;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto left = std::get<TypeClass>(leftSyn);
|
||||||
|
auto right = std::get<TypeClass>(rightSyn);
|
||||||
|
|
||||||
|
switch (this->op_)
|
||||||
|
{
|
||||||
|
case PLUS:
|
||||||
|
if (left == Type::String)
|
||||||
|
return TypeClass{Type::String}; // String concatenation
|
||||||
|
else if (left == Type::Int && right == Type::Int)
|
||||||
|
return TypeClass{Type::Int};
|
||||||
|
|
||||||
|
return IllTyped{this, "Can only add Ints or concatenate a String"};
|
||||||
|
case MINUS:
|
||||||
|
case MULTIPLY:
|
||||||
|
case DIVIDE:
|
||||||
|
case MOD:
|
||||||
|
if (left == Type::Int && right == Type::Int)
|
||||||
|
return TypeClass{Type::Int};
|
||||||
|
|
||||||
|
return IllTyped{this, "Can only perform operation with Ints"};
|
||||||
|
case OR:
|
||||||
|
case AND:
|
||||||
|
if (left == Type::Bool && right == Type::Bool)
|
||||||
|
return TypeClass{Type::Bool};
|
||||||
|
|
||||||
|
return IllTyped{this,
|
||||||
|
"Can only perform logical operations with Bools"};
|
||||||
|
case EQ:
|
||||||
|
case NEQ:
|
||||||
|
// equals/not equals always produces a valid output
|
||||||
|
return TypeClass{Type::Bool};
|
||||||
|
case LT:
|
||||||
|
case GT:
|
||||||
|
case LTE:
|
||||||
|
case GTE:
|
||||||
|
if (left == Type::Int && right == Type::Int)
|
||||||
|
return TypeClass{Type::Bool};
|
||||||
|
|
||||||
|
return IllTyped{this, "Can only perform comparisons with Ints"};
|
||||||
|
case STARTS_WITH:
|
||||||
|
case ENDS_WITH:
|
||||||
|
if (isList(left))
|
||||||
|
return TypeClass{Type::Bool};
|
||||||
|
if (left == Type::String && right == Type::String)
|
||||||
|
return TypeClass{Type::Bool};
|
||||||
|
|
||||||
|
return IllTyped{
|
||||||
|
this,
|
||||||
|
"Can only perform starts/ends with a List or two Strings"};
|
||||||
|
case CONTAINS:
|
||||||
|
if (isList(left) || left == Type::Map)
|
||||||
|
return TypeClass{Type::Bool};
|
||||||
|
if (left == Type::String && right == Type::String)
|
||||||
|
return TypeClass{Type::Bool};
|
||||||
|
|
||||||
|
return IllTyped{
|
||||||
|
this,
|
||||||
|
"Can only perform contains with a List, a Map, or two Strings"};
|
||||||
|
case MATCH: {
|
||||||
|
if (left != Type::String)
|
||||||
|
return IllTyped{this,
|
||||||
|
"Left argument of match must be a String"};
|
||||||
|
|
||||||
|
if (right == Type::RegularExpression)
|
||||||
|
return TypeClass{Type::Bool};
|
||||||
|
if (right == Type::MatchingSpecifier) // group capturing
|
||||||
|
return TypeClass{Type::String};
|
||||||
|
|
||||||
|
return IllTyped{this, "Can only match on a RegularExpression or a "
|
||||||
|
"MatchingSpecifier"};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return IllTyped{this, "Not implemented"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString BinaryOperation::debug(const TypingContext &context) const
|
||||||
|
{
|
||||||
|
return QString("BinaryOp[%1](%2 : %3, %4 : %5)")
|
||||||
|
.arg(tokenTypeToInfoString(this->op_))
|
||||||
|
.arg(this->left_->debug(context))
|
||||||
|
.arg(possibleTypeToString(this->left_->synthesizeType(context)))
|
||||||
|
.arg(this->right_->debug(context))
|
||||||
|
.arg(possibleTypeToString(this->right_->synthesizeType(context)));
|
||||||
}
|
}
|
||||||
|
|
||||||
QString BinaryOperation::filterString() const
|
QString BinaryOperation::filterString() const
|
||||||
@@ -456,57 +345,14 @@ QString BinaryOperation::filterString() const
|
|||||||
case MATCH:
|
case MATCH:
|
||||||
return "match";
|
return "match";
|
||||||
default:
|
default:
|
||||||
return QString();
|
return "";
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
return QString("(%1) %2 (%3)")
|
return QString("(%1 %2 %3)")
|
||||||
.arg(this->left_->filterString())
|
.arg(this->left_->filterString())
|
||||||
.arg(opText)
|
.arg(opText)
|
||||||
.arg(this->right_->filterString());
|
.arg(this->right_->filterString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnaryOperation
|
} // namespace chatterino::filters
|
||||||
|
|
||||||
UnaryOperation::UnaryOperation(TokenType op, ExpressionPtr right)
|
|
||||||
: op_(op)
|
|
||||||
, right_(std::move(right))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant UnaryOperation::execute(const ContextMap &context) const
|
|
||||||
{
|
|
||||||
auto right = this->right_->execute(context);
|
|
||||||
switch (this->op_)
|
|
||||||
{
|
|
||||||
case NOT:
|
|
||||||
if (right.canConvert<bool>())
|
|
||||||
return !right.toBool();
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString UnaryOperation::debug() const
|
|
||||||
{
|
|
||||||
return QString("(%1 %2)").arg(tokenTypeToInfoString(this->op_),
|
|
||||||
this->right_->debug());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString UnaryOperation::filterString() const
|
|
||||||
{
|
|
||||||
const auto opText = [&]() -> QString {
|
|
||||||
switch (this->op_)
|
|
||||||
{
|
|
||||||
case NOT:
|
|
||||||
return "!";
|
|
||||||
default:
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
return QString("%1(%2)").arg(opText).arg(this->right_->filterString());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace filterparser
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
class BinaryOperation : public Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
BinaryOperation(TokenType op, ExpressionPtr left, ExpressionPtr right);
|
||||||
|
|
||||||
|
QVariant execute(const ContextMap &context) const override;
|
||||||
|
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||||
|
QString debug(const TypingContext &context) const override;
|
||||||
|
QString filterString() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TokenType op_;
|
||||||
|
ExpressionPtr left_;
|
||||||
|
ExpressionPtr right_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
QVariant Expression::execute(const ContextMap & /*context*/) const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PossibleType Expression::synthesizeType(const TypingContext & /*context*/) const
|
||||||
|
{
|
||||||
|
return IllTyped{this, "Not implemented"};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Expression::debug(const TypingContext & /*context*/) const
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Expression::filterString() const
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
class Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~Expression() = default;
|
||||||
|
|
||||||
|
virtual QVariant execute(const ContextMap &context) const;
|
||||||
|
virtual PossibleType synthesizeType(const TypingContext &context) const;
|
||||||
|
virtual QString debug(const TypingContext &context) const;
|
||||||
|
virtual QString filterString() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ExpressionPtr = std::unique_ptr<Expression>;
|
||||||
|
using ExpressionList = std::vector<std::unique_ptr<Expression>>;
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#include "controllers/filters/lang/expressions/ListExpression.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
ListExpression::ListExpression(ExpressionList &&list)
|
||||||
|
: list_(std::move(list)){};
|
||||||
|
|
||||||
|
QVariant ListExpression::execute(const ContextMap &context) const
|
||||||
|
{
|
||||||
|
QList<QVariant> results;
|
||||||
|
bool allStrings = true;
|
||||||
|
for (const auto &exp : this->list_)
|
||||||
|
{
|
||||||
|
auto res = exp->execute(context);
|
||||||
|
if (allStrings && variantIsNot(res.type(), QMetaType::QString))
|
||||||
|
{
|
||||||
|
allStrings = false;
|
||||||
|
}
|
||||||
|
results.append(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if everything is a string return a QStringList for case-insensitive comparison
|
||||||
|
if (allStrings)
|
||||||
|
{
|
||||||
|
QStringList strings;
|
||||||
|
strings.reserve(results.size());
|
||||||
|
for (const auto &val : results)
|
||||||
|
{
|
||||||
|
strings << val.toString();
|
||||||
|
}
|
||||||
|
return strings;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
PossibleType ListExpression::synthesizeType(const TypingContext &context) const
|
||||||
|
{
|
||||||
|
std::vector<TypeClass> types;
|
||||||
|
types.reserve(this->list_.size());
|
||||||
|
bool allStrings = true;
|
||||||
|
for (const auto &exp : this->list_)
|
||||||
|
{
|
||||||
|
auto typSyn = exp->synthesizeType(context);
|
||||||
|
if (isIllTyped(typSyn))
|
||||||
|
{
|
||||||
|
return typSyn; // Ill-typed
|
||||||
|
}
|
||||||
|
|
||||||
|
auto typ = std::get<TypeClass>(typSyn);
|
||||||
|
|
||||||
|
if (typ != Type::String)
|
||||||
|
{
|
||||||
|
allStrings = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
types.push_back(typ);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (types.size() == 2 && types[0] == Type::RegularExpression &&
|
||||||
|
types[1] == Type::Int)
|
||||||
|
{
|
||||||
|
// Specific {RegularExpression, Int} form
|
||||||
|
return TypeClass{Type::MatchingSpecifier};
|
||||||
|
}
|
||||||
|
|
||||||
|
return allStrings ? TypeClass{Type::StringList} : TypeClass{Type::List};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ListExpression::debug(const TypingContext &context) const
|
||||||
|
{
|
||||||
|
QStringList debugs;
|
||||||
|
for (const auto &exp : this->list_)
|
||||||
|
{
|
||||||
|
debugs.append(
|
||||||
|
QString("%1 : %2")
|
||||||
|
.arg(exp->debug(context))
|
||||||
|
.arg(possibleTypeToString(exp->synthesizeType(context))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString("List(%1)").arg(debugs.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ListExpression::filterString() const
|
||||||
|
{
|
||||||
|
QStringList strings;
|
||||||
|
for (const auto &exp : this->list_)
|
||||||
|
{
|
||||||
|
strings.append(exp->filterString());
|
||||||
|
}
|
||||||
|
return QString("{%1}").arg(strings.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
class ListExpression : public Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ListExpression(ExpressionList &&list);
|
||||||
|
|
||||||
|
QVariant execute(const ContextMap &context) const override;
|
||||||
|
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||||
|
QString debug(const TypingContext &context) const override;
|
||||||
|
QString filterString() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ExpressionList list_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#include "controllers/filters/lang/expressions/RegexExpression.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
RegexExpression::RegexExpression(const QString ®ex, bool caseInsensitive)
|
||||||
|
: regexString_(regex)
|
||||||
|
, caseInsensitive_(caseInsensitive)
|
||||||
|
, regex_(QRegularExpression(
|
||||||
|
regex, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
|
||||||
|
: QRegularExpression::NoPatternOption)){};
|
||||||
|
|
||||||
|
QVariant RegexExpression::execute(const ContextMap & /*context*/) const
|
||||||
|
{
|
||||||
|
return this->regex_;
|
||||||
|
}
|
||||||
|
|
||||||
|
PossibleType RegexExpression::synthesizeType(
|
||||||
|
const TypingContext & /*context*/) const
|
||||||
|
{
|
||||||
|
return TypeClass{Type::RegularExpression};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RegexExpression::debug(const TypingContext & /*context*/) const
|
||||||
|
{
|
||||||
|
return QString("RegEx(%1)").arg(this->regexString_);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RegexExpression::filterString() const
|
||||||
|
{
|
||||||
|
auto s = this->regexString_;
|
||||||
|
return QString("%1\"%2\"")
|
||||||
|
.arg(this->caseInsensitive_ ? "ri" : "r")
|
||||||
|
.arg(s.replace("\"", "\\\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
class RegexExpression : public Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RegexExpression(const QString ®ex, bool caseInsensitive);
|
||||||
|
|
||||||
|
QVariant execute(const ContextMap &context) const override;
|
||||||
|
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||||
|
QString debug(const TypingContext &context) const override;
|
||||||
|
QString filterString() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString regexString_;
|
||||||
|
bool caseInsensitive_;
|
||||||
|
QRegularExpression regex_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include "controllers/filters/lang/expressions/UnaryOperation.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
UnaryOperation::UnaryOperation(TokenType op, ExpressionPtr right)
|
||||||
|
: op_(op)
|
||||||
|
, right_(std::move(right))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant UnaryOperation::execute(const ContextMap &context) const
|
||||||
|
{
|
||||||
|
auto right = this->right_->execute(context);
|
||||||
|
switch (this->op_)
|
||||||
|
{
|
||||||
|
case NOT:
|
||||||
|
return right.canConvert<bool>() && !right.toBool();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PossibleType UnaryOperation::synthesizeType(const TypingContext &context) const
|
||||||
|
{
|
||||||
|
auto rightSyn = this->right_->synthesizeType(context);
|
||||||
|
if (isIllTyped(rightSyn))
|
||||||
|
{
|
||||||
|
return rightSyn;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto right = std::get<TypeClass>(rightSyn);
|
||||||
|
|
||||||
|
switch (this->op_)
|
||||||
|
{
|
||||||
|
case NOT:
|
||||||
|
if (right == Type::Bool)
|
||||||
|
{
|
||||||
|
return TypeClass{Type::Bool};
|
||||||
|
}
|
||||||
|
return IllTyped{this, "Can only negate boolean values"};
|
||||||
|
default:
|
||||||
|
return IllTyped{this, "Not implemented"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString UnaryOperation::debug(const TypingContext &context) const
|
||||||
|
{
|
||||||
|
return QString("UnaryOp[%1](%2 : %3)")
|
||||||
|
.arg(tokenTypeToInfoString(this->op_))
|
||||||
|
.arg(this->right_->debug(context))
|
||||||
|
.arg(possibleTypeToString(this->right_->synthesizeType(context)));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString UnaryOperation::filterString() const
|
||||||
|
{
|
||||||
|
const auto opText = [&]() -> QString {
|
||||||
|
switch (this->op_)
|
||||||
|
{
|
||||||
|
case NOT:
|
||||||
|
return "!";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
return QString("(%1%2)").arg(opText).arg(this->right_->filterString());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
class UnaryOperation : public Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
UnaryOperation(TokenType op, ExpressionPtr right);
|
||||||
|
|
||||||
|
QVariant execute(const ContextMap &context) const override;
|
||||||
|
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||||
|
QString debug(const TypingContext &context) const override;
|
||||||
|
QString filterString() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TokenType op_;
|
||||||
|
ExpressionPtr right_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#include "controllers/filters/lang/expressions/ValueExpression.hpp"
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
ValueExpression::ValueExpression(QVariant value, TokenType type)
|
||||||
|
: value_(std::move(value))
|
||||||
|
, type_(type)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ValueExpression::execute(const ContextMap &context) const
|
||||||
|
{
|
||||||
|
if (this->type_ == TokenType::IDENTIFIER)
|
||||||
|
{
|
||||||
|
return context.value(this->value_.toString());
|
||||||
|
}
|
||||||
|
return this->value_;
|
||||||
|
}
|
||||||
|
|
||||||
|
PossibleType ValueExpression::synthesizeType(const TypingContext &context) const
|
||||||
|
{
|
||||||
|
switch (this->type_)
|
||||||
|
{
|
||||||
|
case TokenType::IDENTIFIER: {
|
||||||
|
auto it = context.find(this->value_.toString());
|
||||||
|
if (it != context.end())
|
||||||
|
{
|
||||||
|
return TypeClass{it.value()};
|
||||||
|
}
|
||||||
|
|
||||||
|
return IllTyped{this, "Unbound identifier"};
|
||||||
|
}
|
||||||
|
case TokenType::INT:
|
||||||
|
return TypeClass{Type::Int};
|
||||||
|
case TokenType::STRING:
|
||||||
|
return TypeClass{Type::String};
|
||||||
|
default:
|
||||||
|
return IllTyped{this, "Invalid value type"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType ValueExpression::type()
|
||||||
|
{
|
||||||
|
return this->type_;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ValueExpression::debug(const TypingContext & /*context*/) const
|
||||||
|
{
|
||||||
|
return QString("Val(%1)").arg(this->value_.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ValueExpression::filterString() const
|
||||||
|
{
|
||||||
|
switch (this->type_)
|
||||||
|
{
|
||||||
|
case INT:
|
||||||
|
return QString::number(this->value_.toInt());
|
||||||
|
case STRING:
|
||||||
|
return QString("\"%1\"").arg(
|
||||||
|
this->value_.toString().replace("\"", "\\\""));
|
||||||
|
case IDENTIFIER:
|
||||||
|
return this->value_.toString();
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
class ValueExpression : public Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ValueExpression(QVariant value, TokenType type);
|
||||||
|
TokenType type();
|
||||||
|
|
||||||
|
QVariant execute(const ContextMap &context) const override;
|
||||||
|
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||||
|
QString debug(const TypingContext &context) const override;
|
||||||
|
QString filterString() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVariant value_;
|
||||||
|
TokenType type_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QRegularExpression>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace chatterino {
|
|
||||||
|
|
||||||
struct Message;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace filterparser {
|
|
||||||
|
|
||||||
using MessagePtr = std::shared_ptr<const chatterino::Message>;
|
|
||||||
using ContextMap = QMap<QString, QVariant>;
|
|
||||||
|
|
||||||
enum TokenType {
|
|
||||||
// control
|
|
||||||
CONTROL_START = 0,
|
|
||||||
AND = 1,
|
|
||||||
OR = 2,
|
|
||||||
LP = 3,
|
|
||||||
RP = 4,
|
|
||||||
LIST_START = 5,
|
|
||||||
LIST_END = 6,
|
|
||||||
COMMA = 7,
|
|
||||||
CONTROL_END = 19,
|
|
||||||
|
|
||||||
// binary operator
|
|
||||||
BINARY_START = 20,
|
|
||||||
EQ = 21,
|
|
||||||
NEQ = 22,
|
|
||||||
LT = 23,
|
|
||||||
GT = 24,
|
|
||||||
LTE = 25,
|
|
||||||
GTE = 26,
|
|
||||||
CONTAINS = 27,
|
|
||||||
STARTS_WITH = 28,
|
|
||||||
ENDS_WITH = 29,
|
|
||||||
MATCH = 30,
|
|
||||||
BINARY_END = 49,
|
|
||||||
|
|
||||||
// unary operator
|
|
||||||
UNARY_START = 50,
|
|
||||||
NOT = 51,
|
|
||||||
UNARY_END = 99,
|
|
||||||
|
|
||||||
// math operators
|
|
||||||
MATH_START = 100,
|
|
||||||
PLUS = 101,
|
|
||||||
MINUS = 102,
|
|
||||||
MULTIPLY = 103,
|
|
||||||
DIVIDE = 104,
|
|
||||||
MOD = 105,
|
|
||||||
MATH_END = 149,
|
|
||||||
|
|
||||||
// other types
|
|
||||||
OTHER_START = 150,
|
|
||||||
STRING = 151,
|
|
||||||
INT = 152,
|
|
||||||
IDENTIFIER = 153,
|
|
||||||
REGULAR_EXPRESSION = 154,
|
|
||||||
|
|
||||||
NONE = 200
|
|
||||||
};
|
|
||||||
|
|
||||||
bool convertVariantTypes(QVariant &a, QVariant &b, int type);
|
|
||||||
QString tokenTypeToInfoString(TokenType type);
|
|
||||||
|
|
||||||
class Expression
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
virtual ~Expression() = default;
|
|
||||||
|
|
||||||
virtual QVariant execute(const ContextMap &) const
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual QString debug() const
|
|
||||||
{
|
|
||||||
return "(false)";
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual QString filterString() const
|
|
||||||
{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
using ExpressionPtr = std::unique_ptr<Expression>;
|
|
||||||
|
|
||||||
class ValueExpression : public Expression
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
ValueExpression(QVariant value, TokenType type);
|
|
||||||
TokenType type();
|
|
||||||
|
|
||||||
QVariant execute(const ContextMap &context) const override;
|
|
||||||
QString debug() const override;
|
|
||||||
QString filterString() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QVariant value_;
|
|
||||||
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
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
ListExpression(ExpressionList list);
|
|
||||||
|
|
||||||
QVariant execute(const ContextMap &context) const override;
|
|
||||||
QString debug() const override;
|
|
||||||
QString filterString() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
ExpressionList list_;
|
|
||||||
};
|
|
||||||
|
|
||||||
class BinaryOperation : public Expression
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
BinaryOperation(TokenType op, ExpressionPtr left, ExpressionPtr right);
|
|
||||||
|
|
||||||
QVariant execute(const ContextMap &context) const override;
|
|
||||||
QString debug() const override;
|
|
||||||
QString filterString() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
TokenType op_;
|
|
||||||
ExpressionPtr left_;
|
|
||||||
ExpressionPtr right_;
|
|
||||||
};
|
|
||||||
|
|
||||||
class UnaryOperation : public Expression
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
UnaryOperation(TokenType op, ExpressionPtr right);
|
|
||||||
|
|
||||||
QVariant execute(const ContextMap &context) const override;
|
|
||||||
QString debug() const override;
|
|
||||||
QString filterString() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
TokenType op_;
|
|
||||||
ExpressionPtr right_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace filterparser
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "ChannelFilterEditorDialog.hpp"
|
#include "widgets/dialogs/ChannelFilterEditorDialog.hpp"
|
||||||
|
|
||||||
#include "controllers/filters/parser/FilterParser.hpp"
|
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||||
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
@@ -99,7 +99,8 @@ ChannelFilterEditorDialog::ValueSpecifier::ValueSpecifier()
|
|||||||
|
|
||||||
this->typeCombo_->insertItems(
|
this->typeCombo_->insertItems(
|
||||||
0, {"Constant Text", "Constant Number", "Variable"});
|
0, {"Constant Text", "Constant Number", "Variable"});
|
||||||
this->varCombo_->insertItems(0, filterparser::validIdentifiersMap.values());
|
|
||||||
|
this->varCombo_->insertItems(0, filters::validIdentifiersMap.values());
|
||||||
|
|
||||||
this->layout_->addWidget(this->typeCombo_);
|
this->layout_->addWidget(this->typeCombo_);
|
||||||
this->layout_->addWidget(this->varCombo_, 1);
|
this->layout_->addWidget(this->varCombo_, 1);
|
||||||
@@ -141,7 +142,7 @@ void ChannelFilterEditorDialog::ValueSpecifier::setValue(const QString &value)
|
|||||||
if (this->typeCombo_->currentIndex() == 2)
|
if (this->typeCombo_->currentIndex() == 2)
|
||||||
{
|
{
|
||||||
this->varCombo_->setCurrentText(
|
this->varCombo_->setCurrentText(
|
||||||
filterparser::validIdentifiersMap.value(value));
|
filters::validIdentifiersMap.value(value));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -164,7 +165,7 @@ QString ChannelFilterEditorDialog::ValueSpecifier::expressionText()
|
|||||||
case 1: // number
|
case 1: // number
|
||||||
return this->valueInput_->text();
|
return this->valueInput_->text();
|
||||||
case 2: // variable
|
case 2: // variable
|
||||||
return filterparser::validIdentifiersMap.key(
|
return filters::validIdentifiersMap.key(
|
||||||
this->varCombo_->currentText());
|
this->varCombo_->currentText());
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
@@ -221,7 +222,7 @@ QString ChannelFilterEditorDialog::BinaryOperationSpecifier::expressionText()
|
|||||||
return this->left_->expressionText();
|
return this->left_->expressionText();
|
||||||
}
|
}
|
||||||
|
|
||||||
return QString("(%1) %2 (%3)")
|
return QString("(%1 %2 %3)")
|
||||||
.arg(this->left_->expressionText())
|
.arg(this->left_->expressionText())
|
||||||
.arg(opText)
|
.arg(opText)
|
||||||
.arg(this->right_->expressionText());
|
.arg(this->right_->expressionText());
|
||||||
|
|||||||
@@ -91,23 +91,39 @@ void FiltersPage::tableCellClicked(const QModelIndex &clicked,
|
|||||||
{
|
{
|
||||||
QMessageBox popup(this->window());
|
QMessageBox popup(this->window());
|
||||||
|
|
||||||
filterparser::FilterParser f(
|
auto filterText =
|
||||||
view->getModel()->data(clicked.siblingAtColumn(1)).toString());
|
view->getModel()->data(clicked.siblingAtColumn(1)).toString();
|
||||||
|
auto filterResult = filters::Filter::fromString(filterText);
|
||||||
|
|
||||||
if (f.valid())
|
if (std::holds_alternative<filters::Filter>(filterResult))
|
||||||
{
|
{
|
||||||
popup.setIcon(QMessageBox::Icon::Information);
|
auto f = std::move(std::get<filters::Filter>(filterResult));
|
||||||
popup.setWindowTitle("Valid filter");
|
if (f.returnType() == filters::Type::Bool)
|
||||||
popup.setText("Filter is valid");
|
{
|
||||||
popup.setInformativeText(
|
popup.setIcon(QMessageBox::Icon::Information);
|
||||||
QString("Parsed as:\n%1").arg(f.filterString()));
|
popup.setWindowTitle("Valid filter");
|
||||||
|
popup.setText("Filter is valid");
|
||||||
|
popup.setInformativeText(
|
||||||
|
QString("Parsed as:\n%1").arg(f.filterString()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
popup.setIcon(QMessageBox::Icon::Warning);
|
||||||
|
popup.setWindowTitle("Invalid filter");
|
||||||
|
popup.setText(QString("Unexpected filter return type"));
|
||||||
|
popup.setInformativeText(
|
||||||
|
QString("Expected %1 but got %2")
|
||||||
|
.arg(filters::typeToString(filters::Type::Bool))
|
||||||
|
.arg(filters::typeToString(f.returnType())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
auto err = std::move(std::get<filters::FilterError>(filterResult));
|
||||||
popup.setIcon(QMessageBox::Icon::Warning);
|
popup.setIcon(QMessageBox::Icon::Warning);
|
||||||
popup.setWindowTitle("Invalid filter");
|
popup.setWindowTitle("Invalid filter");
|
||||||
popup.setText(QString("Parsing errors occurred:"));
|
popup.setText(QString("Parsing errors occurred:"));
|
||||||
popup.setInformativeText(f.errors().join("\n"));
|
popup.setInformativeText(err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
popup.exec();
|
popup.exec();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ set(test_SOURCES
|
|||||||
${CMAKE_CURRENT_LIST_DIR}/src/SeventvEventAPI.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/SeventvEventAPI.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/BttvLiveUpdates.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/BttvLiveUpdates.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/Updates.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/Updates.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/Filters.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
#include "controllers/filters/lang/Filter.hpp"
|
||||||
|
#include "controllers/filters/lang/Types.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
using namespace chatterino::filters;
|
||||||
|
|
||||||
|
TypingContext typingContext = MESSAGE_TYPING_CONTEXT;
|
||||||
|
|
||||||
|
namespace chatterino::filters {
|
||||||
|
|
||||||
|
std::ostream &operator<<(std::ostream &os, Type t)
|
||||||
|
{
|
||||||
|
os << qUtf8Printable(typeToString(t));
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::filters
|
||||||
|
|
||||||
|
TEST(Filters, Validity)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
QString input;
|
||||||
|
bool valid;
|
||||||
|
};
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
std::vector<TestCase> tests{
|
||||||
|
{"", false},
|
||||||
|
{R".(1 + 1).", true},
|
||||||
|
{R".(1 + ).", false},
|
||||||
|
{R".(1 + 1)).", false},
|
||||||
|
{R".((1 + 1).", false},
|
||||||
|
{R".(author.name contains "icelys").", true},
|
||||||
|
{R".(author.color == "#ff0000").", true},
|
||||||
|
{R".(author.name - 5).", false}, // can't perform String - Int
|
||||||
|
{R".(message.content match {r"(\d\d)/(\d\d)/(\d\d\d\d)", 3}).", true},
|
||||||
|
{R".("abc" + 123 == "abc123").", true},
|
||||||
|
{R".(123 + "abc" == "hello").", false},
|
||||||
|
{R".(flags.reply && flags.automod).", true},
|
||||||
|
{R".(unknown.identifier).", false},
|
||||||
|
{R".(channel.name == "forsen" && author.badges contains "moderator").", true},
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
for (const auto &[input, expected] : tests)
|
||||||
|
{
|
||||||
|
auto filterResult = Filter::fromString(input);
|
||||||
|
bool isValid = std::holds_alternative<Filter>(filterResult);
|
||||||
|
EXPECT_EQ(isValid, expected)
|
||||||
|
<< "Filter::fromString( " << qUtf8Printable(input)
|
||||||
|
<< " ) should be " << (expected ? "valid" : "invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Filters, TypeSynthesis)
|
||||||
|
{
|
||||||
|
using T = Type;
|
||||||
|
struct TestCase {
|
||||||
|
QString input;
|
||||||
|
T type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
std::vector<TestCase> tests
|
||||||
|
{
|
||||||
|
{R".(1 + 1).", T::Int},
|
||||||
|
{R".(author.color).", T::Color},
|
||||||
|
{R".(author.name).", T::String},
|
||||||
|
{R".(!author.subbed).", T::Bool},
|
||||||
|
{R".(author.badges).", T::StringList},
|
||||||
|
{R".(channel.name == "forsen" && author.badges contains "moderator").", T::Bool},
|
||||||
|
{R".(message.content match {r"(\d\d)/(\d\d)/(\d\d\d\d)", 3}).", T::String},
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
for (const auto &[input, expected] : tests)
|
||||||
|
{
|
||||||
|
auto filterResult = Filter::fromString(input);
|
||||||
|
bool isValid = std::holds_alternative<Filter>(filterResult);
|
||||||
|
ASSERT_TRUE(isValid) << "Filter::fromString( " << qUtf8Printable(input)
|
||||||
|
<< " ) is invalid";
|
||||||
|
|
||||||
|
auto filter = std::move(std::get<Filter>(filterResult));
|
||||||
|
T type = filter.returnType();
|
||||||
|
EXPECT_EQ(type, expected)
|
||||||
|
<< "Filter{ " << qUtf8Printable(input) << " } has type " << type
|
||||||
|
<< " instead of " << expected << ".\nDebug: "
|
||||||
|
<< qUtf8Printable(filter.debugString(typingContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Filters, Evaluation)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
QString input;
|
||||||
|
QVariant output;
|
||||||
|
};
|
||||||
|
|
||||||
|
ContextMap contextMap = {
|
||||||
|
{"author.name", QVariant("icelys")},
|
||||||
|
{"author.color", QVariant(QColor("#ff0000"))},
|
||||||
|
{"author.subbed", QVariant(false)},
|
||||||
|
{"message.content", QVariant("hey there :) 2038-01-19 123 456")},
|
||||||
|
{"channel.name", QVariant("forsen")},
|
||||||
|
{"author.badges", QVariant(QStringList({"moderator", "staff"}))}};
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
std::vector<TestCase> tests
|
||||||
|
{
|
||||||
|
// Evaluation semantics
|
||||||
|
{R".(1 + 1).", QVariant(2)},
|
||||||
|
{R".(!(1 == 1)).", QVariant(false)},
|
||||||
|
{R".(2 + 3 * 4).", QVariant(20)}, // math operators have the same precedence
|
||||||
|
{R".(1 > 2 || 3 >= 3).", QVariant(true)},
|
||||||
|
{R".(1 > 2 && 3 > 1).", QVariant(false)},
|
||||||
|
{R".("abc" + 123).", QVariant("abc123")},
|
||||||
|
{R".("abc" + "456").", QVariant("abc456")},
|
||||||
|
{R".(3 - 4).", QVariant(-1)},
|
||||||
|
{R".(3 * 4).", QVariant(12)},
|
||||||
|
{R".(8 / 3).", QVariant(2)},
|
||||||
|
{R".(7 % 3).", QVariant(1)},
|
||||||
|
{R".(5 == 5).", QVariant(true)},
|
||||||
|
{R".(5 == "5").", QVariant(true)},
|
||||||
|
{R".(5 != 7).", QVariant(true)},
|
||||||
|
{R".(5 == "abc").", QVariant(false)},
|
||||||
|
{R".("ABC123" == "abc123").", QVariant(true)}, // String comparison is case-insensitive
|
||||||
|
{R".("Hello world" contains "Hello").", QVariant(true)},
|
||||||
|
{R".("Hello world" contains "LLO W").", QVariant(true)}, // Case-insensitive
|
||||||
|
{R".({"abc", "def"} contains "abc").", QVariant(true)},
|
||||||
|
{R".({"abc", "def"} contains "ABC").", QVariant(true)}, // Case-insensitive when list is all strings
|
||||||
|
{R".({123, "def"} contains "DEF").", QVariant(false)}, // Case-sensitive if list not all strings
|
||||||
|
{R".({"a123", "b456"} startswith "a123").", QVariant(true)},
|
||||||
|
{R".({"a123", "b456"} startswith "A123").", QVariant(true)},
|
||||||
|
{R".({} startswith "A123").", QVariant(false)},
|
||||||
|
{R".("Hello world" startswith "Hello").", QVariant(true)},
|
||||||
|
{R".("Hello world" startswith "world").", QVariant(false)},
|
||||||
|
{R".({"a123", "b456"} endswith "b456").", QVariant(true)},
|
||||||
|
{R".({"a123", "b456"} endswith "B456").", QVariant(true)},
|
||||||
|
{R".("Hello world" endswith "world").", QVariant(true)},
|
||||||
|
{R".("Hello world" endswith "Hello").", QVariant(false)},
|
||||||
|
// Context map usage
|
||||||
|
{R".(author.name).", QVariant("icelys")},
|
||||||
|
{R".(!author.subbed).", QVariant(true)},
|
||||||
|
{R".(author.color == "#ff0000").", QVariant(true)},
|
||||||
|
{R".(channel.name == "forsen" && author.badges contains "moderator").", QVariant(true)},
|
||||||
|
{R".(message.content match {r"(\d\d\d\d)\-(\d\d)\-(\d\d)", 3}).", QVariant("19")},
|
||||||
|
{R".(message.content match r"HEY THERE").", QVariant(false)},
|
||||||
|
{R".(message.content match ri"HEY THERE").", QVariant(true)},
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
for (const auto &[input, expected] : tests)
|
||||||
|
{
|
||||||
|
auto filterResult = Filter::fromString(input);
|
||||||
|
bool isValid = std::holds_alternative<Filter>(filterResult);
|
||||||
|
ASSERT_TRUE(isValid) << "Filter::fromString( " << qUtf8Printable(input)
|
||||||
|
<< " ) is invalid";
|
||||||
|
|
||||||
|
auto filter = std::move(std::get<Filter>(filterResult));
|
||||||
|
auto result = filter.execute(contextMap);
|
||||||
|
|
||||||
|
EXPECT_EQ(result, expected)
|
||||||
|
<< "Filter{ " << qUtf8Printable(input) << " } evaluated to "
|
||||||
|
<< qUtf8Printable(result.toString()) << " instead of "
|
||||||
|
<< qUtf8Printable(expected.toString()) << ".\nDebug: "
|
||||||
|
<< qUtf8Printable(filter.debugString(typingContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user