common: Port HighlightRule to ExpressionMatch
[quassel.git] / src / common / highlightrulemanager.cpp
index ff60df6..c9c4cfc 100644 (file)
@@ -22,6 +22,7 @@
 
 #include <QDebug>
 
+#include "expressionmatch.h"
 #include "util.h"
 
 INIT_SYNCABLE_OBJECT(HighlightRuleManager)
@@ -42,7 +43,7 @@ HighlightRuleManager &HighlightRuleManager::operator=(const HighlightRuleManager
 int HighlightRuleManager::indexOf(int id) const
 {
     for (int i = 0; i < _highlightRuleList.count(); i++) {
-        if (_highlightRuleList[i].id == id)
+        if (_highlightRuleList[i].id() == id)
             return i;
     }
     return -1;
@@ -53,7 +54,7 @@ int HighlightRuleManager::nextId()
 {
     int max = 0;
     for (int i = 0; i < _highlightRuleList.count(); i++) {
-        int id = _highlightRuleList[i].id;
+        int id = _highlightRuleList[i].id();
         if (id > max) {
             max = id;
         }
@@ -75,14 +76,14 @@ QVariantMap HighlightRuleManager::initHighlightRuleList() const
     QStringList channel;
 
     for (int i = 0; i < _highlightRuleList.count(); i++) {
-        id << _highlightRuleList[i].id;
-        name << _highlightRuleList[i].name;
-        isRegEx << _highlightRuleList[i].isRegEx;
-        isCaseSensitive << _highlightRuleList[i].isCaseSensitive;
-        isActive << _highlightRuleList[i].isEnabled;
-        isInverse << _highlightRuleList[i].isInverse;
-        sender << _highlightRuleList[i].sender;
-        channel << _highlightRuleList[i].chanName;
+        id << _highlightRuleList[i].id();
+        name << _highlightRuleList[i].contents();
+        isRegEx << _highlightRuleList[i].isRegEx();
+        isCaseSensitive << _highlightRuleList[i].isCaseSensitive();
+        isActive << _highlightRuleList[i].isEnabled();
+        isInverse << _highlightRuleList[i].isInverse();
+        sender << _highlightRuleList[i].sender();
+        channel << _highlightRuleList[i].chanName();
     }
 
     highlightRuleListMap["id"] = id;
@@ -155,47 +156,34 @@ bool HighlightRuleManager::match(const QString &msgContents,
     bool matches = false;
 
     for (int i = 0; i < _highlightRuleList.count(); i++) {
-        const HighlightRule &rule = _highlightRuleList.at(i);
-        if (!rule.isEnabled)
+        auto &rule = _highlightRuleList.at(i);
+        if (!rule.isEnabled())
             continue;
 
-        if (!rule.chanName.isEmpty()
-                && !scopeMatch(bufferName, rule.chanName, rule.isRegEx, rule.isCaseSensitive)) {
+        // Skip if channel name doesn't match and channel rule is not empty
+        //
+        // Match succeeds if...
+        //   Channel name matches a defined rule
+        //   Defined rule is empty
+        // And take the inverse of the above
+        if (!rule.chanNameMatcher().match(bufferName, true)) {
             // A channel name rule is specified and does NOT match the current buffer name, skip
             // this rule
             continue;
         }
 
-        bool nameMatch = false;
-        if (rule.name.isEmpty()) {
-            // Empty rule, matches any message
-            nameMatch = true;
-        } else {
-            // Check according to specified rule
-            QRegExp rx;
-            if (rule.isRegEx) {
-                rx = QRegExp(rule.name,
-                             rule.isCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
-            } else {
-                rx = QRegExp("(^|\\W)" + QRegExp::escape(rule.name) + "(\\W|$)",
-                             rule.isCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
-            }
-            nameMatch = (rx.indexIn(stripFormatCodes(msgContents)) >= 0);
-        }
+        // Check message according to specified rule, allowing empty rules to match
+        bool contentsMatch = rule.contentsMatcher().match(stripFormatCodes(msgContents), true);
 
-        bool senderMatch;
-        if (rule.sender.isEmpty()) {
-            senderMatch = true;
-        } else {
-            // A sender name rule is specified, match according to scope rules.
-            senderMatch = scopeMatch(msgSender, rule.sender, rule.isRegEx, rule.isCaseSensitive);
-        }
+        // Check sender according to specified rule, allowing empty rules to match
+        bool senderMatch = rule.senderMatcher().match(msgSender, true);
 
-        if (nameMatch && senderMatch) {
+        if (contentsMatch && senderMatch) {
             // If an inverse rule matches, then we know that we never want to return a highlight.
-            if (rule.isInverse) {
+            if (rule.isInverse()) {
                 return false;
-            } else {
+            }
+            else {
                 matches = true;
             }
         }
@@ -204,22 +192,16 @@ bool HighlightRuleManager::match(const QString &msgContents,
     if (matches)
         return true;
 
-    if (!currentNick.isEmpty()) {
-        QStringList nickList;
-        if (_highlightNick == CurrentNick) {
-            nickList << currentNick;
-        }
-        else if (_highlightNick == AllNicks) {
-            nickList = identityNicks;
-            if (!nickList.contains(currentNick))
-                nickList.prepend(currentNick);
-        }
+    // Check nicknames
+    if (_highlightNick != HighlightNickType::NoNick && !currentNick.isEmpty()) {
+        // Update cache if needed
+        determineNickExpressions(currentNick, identityNicks);
 
-        for(const QString &nickname : nickList) {
-            QRegExp nickRegExp("(^|\\W)" + QRegExp::escape(nickname) + "(\\W|$)", _nicksCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
-            if (nickRegExp.indexIn(stripFormatCodes(msgContents)) >= 0) {
-                return true;
-            }
+        // Check for a match
+        if (_cachedNickMatcher.isValid()
+                && _cachedNickMatcher.match(stripFormatCodes(msgContents))) {
+            // Nick matcher is valid and match found
+            return true;
         }
     }
 
@@ -239,7 +221,7 @@ void HighlightRuleManager::toggleHighlightRule(int highlightRule)
     int idx = indexOf(highlightRule);
     if (idx == -1)
         return;
-    _highlightRuleList[idx].isEnabled = !_highlightRuleList[idx].isEnabled;
+    _highlightRuleList[idx].setIsEnabled(!_highlightRuleList[idx].isEnabled());
     SYNC(ARG(highlightRule))
 }
 
@@ -248,3 +230,82 @@ bool HighlightRuleManager::match(const Message &msg, const QString &currentNick,
 {
     return match(msg.contents(), msg.sender(), msg.type(), msg.flags(), msg.bufferInfo().bufferName(), currentNick, identityNicks);
 }
+
+
+void HighlightRuleManager::determineNickExpressions(const QString &currentNick,
+                                                    const QStringList identityNicks) const
+{
+    // Don't do anything for no nicknames
+    if (_highlightNick == HighlightNickType::NoNick) {
+        return;
+    }
+
+    // Only update if needed (check nickname config, current nick, identity nicks for change)
+    if (!_cacheNickConfigInvalid
+          && _cachedNickCurrent == currentNick
+          && _cachedIdentityNicks == identityNicks) {
+        return;
+    }
+
+    // Add all nicknames
+    QStringList nickList;
+    if (_highlightNick == CurrentNick) {
+        nickList << currentNick;
+    }
+    else if (_highlightNick == AllNicks) {
+        nickList = identityNicks;
+        if (!nickList.contains(currentNick))
+            nickList.prepend(currentNick);
+    }
+
+    // Set up phrase matcher, joining with newlines
+    _cachedNickMatcher = ExpressionMatch(nickList.join("\n"),
+                                        ExpressionMatch::MatchMode::MatchMultiPhrase,
+                                        _nicksCaseSensitive);
+
+    _cacheNickConfigInvalid = false;
+    _cachedNickCurrent = currentNick;
+    _cachedIdentityNicks = identityNicks;
+}
+
+
+/**************************************************************************
+ * HighlightRule
+ *************************************************************************/
+bool HighlightRuleManager::HighlightRule::operator!=(const HighlightRule &other) const
+{
+    return (_id != other._id ||
+            _contents != other._contents ||
+            _isRegEx != other._isRegEx ||
+            _isCaseSensitive != other._isCaseSensitive ||
+            _isEnabled != other._isEnabled ||
+            _isInverse != other._isInverse ||
+            _sender != other._sender ||
+            _chanName != other._chanName);
+    // Don't compare ExpressionMatch objects as they are created as needed from the above
+}
+
+
+void HighlightRuleManager::HighlightRule::determineExpressions() const
+{
+    // Don't update if not needed
+    if (!_cacheInvalid) {
+        return;
+    }
+
+    // Set up matching rules
+    // Message is either phrase or regex
+    ExpressionMatch::MatchMode contentsMode =
+            _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx :
+                       ExpressionMatch::MatchMode::MatchPhrase;
+    // Sender and channel are either multiple wildcard entries or regex
+    ExpressionMatch::MatchMode scopeMode =
+            _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx :
+                       ExpressionMatch::MatchMode::MatchMultiWildcard;
+
+    _contentsMatch = ExpressionMatch(_contents, contentsMode, _isCaseSensitive);
+    _senderMatch = ExpressionMatch(_sender, scopeMode, _isCaseSensitive);
+    _chanNameMatch = ExpressionMatch(_chanName, scopeMode, _isCaseSensitive);
+
+    _cacheInvalid = false;
+}