reapack

Package manager for REAPER
Log | Files | Refs | Submodules | README | LICENSE

filter.cpp (7118B)


      1 /* ReaPack: Package manager for REAPER
      2  * Copyright (C) 2015-2025  Christian Fillion
      3  *
      4  * This program is free software: you can redistribute it and/or modify
      5  * it under the terms of the GNU Lesser General Public License as published by
      6  * the Free Software Foundation, either version 3 of the License, or
      7  * (at your option) any later version.
      8  *
      9  * This program is distributed in the hope that it will be useful,
     10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12  * GNU Lesser General Public License for more details.
     13  *
     14  * You should have received a copy of the GNU Lesser General Public License
     15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16  */
     17 
     18 #include "filter.hpp"
     19 
     20 #include "config.hpp"
     21 #include "reapack.hpp"
     22 
     23 #include <boost/algorithm/string.hpp>
     24 
     25 Filter::Filter(const std::string &input)
     26   : m_root(Group::MatchAll)
     27 {
     28   set(input);
     29 }
     30 
     31 void Filter::set(const std::string &input)
     32 {
     33   m_input = input;
     34   m_root.clear();
     35 
     36   std::string_view buf;
     37   char quote = 0;
     38   int flags = 0;
     39   Group *group = &m_root;
     40 
     41   for(size_t i = 0; i < m_input.size(); ++i) {
     42     const char &c = m_input[i];
     43 
     44     const bool isStart = buf.empty(),
     45                isEnd = i+1 == m_input.size() || m_input[i+1] == '\x20';
     46 
     47     if((c == '"' || c == '\'') && ((!quote && isStart) || quote == c)) {
     48       if(quote)
     49         quote = 0;
     50       else {
     51         flags |= Node::LiteralFlag | Node::FullWordFlag;
     52         quote = c;
     53       }
     54       continue;
     55     }
     56     else if(c == '\x20') {
     57       if(quote)
     58         flags &= ~Node::FullWordFlag;
     59       else {
     60         group = group->push(buf, &flags);
     61         buf = {};
     62         continue;
     63       }
     64     }
     65     else if(!quote) {
     66       if(c == '^' && isStart) {
     67         flags |= Node::StartAnchorFlag;
     68         continue;
     69       }
     70       else if(c == '$' && isEnd) {
     71         flags |= Node::EndAnchorFlag;
     72         continue;
     73       }
     74       else if(flags & Node::LiteralFlag) {
     75         // force-close the token after having parsed a closing quote
     76         // and only after having parsed all trailing anchors
     77         group = group->push(buf, &flags);
     78         buf = {};
     79       }
     80     }
     81 
     82     if(buf.empty())
     83       buf = { &c, 1 };
     84     else
     85       buf = { buf.data(), buf.size() + 1 };
     86   }
     87 
     88   group->push(buf, &flags);
     89 }
     90 
     91 bool Filter::match(std::vector<std::string> rows) const
     92 {
     93   for(std::string &str : rows)
     94     boost::algorithm::to_lower(str);
     95 
     96   return m_root.match(rows);
     97 }
     98 
     99 static void convertToLower(const std::string_view &buf)
    100 {
    101   char *data = const_cast<char *>(buf.data());
    102   std::transform(data, data + buf.size(), data,
    103     [](unsigned char c){ return std::tolower(c); });
    104 }
    105 
    106 Filter::Group::Group(Type type, int flags, Group *parent)
    107   : Node(flags), m_parent(parent), m_type(type)
    108 {
    109 }
    110 
    111 Filter::Group *Filter::Group::push(const std::string_view &buf, int *flags)
    112 {
    113   if(buf.empty())
    114     return this;
    115 
    116   if(!(*flags & LiteralFlag)) {
    117     if(buf == "NOT") {
    118       *flags ^= NotFlag;
    119       return this;
    120     }
    121     else if(buf == "OR") {
    122       if(m_nodes.empty())
    123         return this; // no previous token, ignore
    124 
    125       Group *currentOr = dynamic_cast<Group *>(m_nodes.back().get());
    126       if(currentOr && currentOr->m_type == MatchAny)
    127         return currentOr;
    128 
    129       auto prev = std::move(m_nodes.back());
    130       m_nodes.pop_back();
    131 
    132       Group *newGroup = addSubGroup(MatchAny, 0);
    133       newGroup->m_nodes.push_back(std::move(prev));
    134       return newGroup;
    135     }
    136     else if(buf == "(") {
    137       Group *newGroup = addSubGroup(MatchAll, *flags);
    138       *flags = 0;
    139       return newGroup;
    140     }
    141     else if(buf == ")") {
    142       for(Group *parent = m_parent; parent; parent = parent->m_parent) {
    143         if(parent->m_type == MatchAll)
    144           return parent;
    145       }
    146 
    147       return this;
    148     }
    149     else if((!g_reapack || g_reapack->config()->filter.expandSynonyms) &&
    150             pushSynonyms(buf, flags))
    151       return this;
    152   }
    153 
    154   convertToLower(buf);
    155   m_nodes.push_back(std::make_unique<Token>(buf, *flags));
    156   *flags = 0;
    157 
    158   Group *group = this;
    159   while(group->m_type != MatchAll && group->m_parent)
    160     group = group->m_parent;
    161   return group;
    162 }
    163 
    164 Filter::Group *Filter::Group::addSubGroup(const Type type, const int flags)
    165 {
    166   auto newGroup = std::make_unique<Group>(type, flags, this);
    167   Group *ptr = newGroup.get();
    168   m_nodes.push_back(std::move(newGroup));
    169 
    170   return ptr;
    171 }
    172 
    173 bool Filter::Group::pushSynonyms(const std::string_view &buf, int *flags)
    174 {
    175   // from the [actionlist_synonyms] section in REAPER's langpack
    176   static const std::vector<std::string_view> synonyms[] {
    177     { "open", "display", "view", "show", "hide" },
    178     { "delete", "clear", "remove", "erase" },
    179     { "insert", "add" },
    180     { "deselect", "unselect" },
    181     { "color", "colour" },
    182     { "colors", "colours" },
    183     { "normalize", "normalise" },
    184     { "normalized", "normalised" },
    185     { "customize", "customise" },
    186     { "synchronize", "synchronise" },
    187     { "optimize", "optimise" },
    188     { "optimized", "optimised" },
    189     { "center", "centre" },
    190     { "join", "heal" },
    191     { "during", "while" },
    192     { "2nd", "second" },
    193     { "unpool", "un-pool" },
    194     { "spacer", "separator" },
    195   };
    196 
    197   auto *match = [&]() -> decltype(&*synonyms) {
    198     for(const auto &synonym : synonyms) {
    199       for(const auto &word : synonym) {
    200         if(boost::iequals(buf, word))
    201           return &synonym;
    202       }
    203     }
    204     return nullptr;
    205   }();
    206 
    207   if(!match)
    208     return false;
    209 
    210   Group *notGroup;
    211   if(*flags & NotFlag) {
    212     notGroup = addSubGroup(MatchAll, NotFlag);
    213     *flags ^= NotFlag;
    214   }
    215   else
    216     notGroup = this;
    217 
    218   Group *orGroup = notGroup->addSubGroup(MatchAny, 0);
    219   if(!(*flags & FullWordFlag)) {
    220     convertToLower(buf);
    221     orGroup->m_nodes.push_back(std::make_unique<Token>(buf, *flags));
    222   }
    223   for(const auto &word : *match)
    224     orGroup->m_nodes.push_back(std::make_unique<Token>(word, *flags | FullWordFlag));
    225 
    226   *flags = 0;
    227   return true;
    228 }
    229 
    230 bool Filter::Group::match(const std::vector<std::string> &rows) const
    231 {
    232   for(const auto &node : m_nodes) {
    233     if(node->match(rows)) {
    234       if(m_type == MatchAny)
    235         return true;
    236     }
    237     else if(m_type == MatchAll)
    238       return test(NotFlag);
    239   }
    240 
    241   return m_type == MatchAll && !test(NotFlag);
    242 }
    243 
    244 Filter::Token::Token(const std::string_view &buf, int flags)
    245   : Node(flags), m_buf(buf)
    246 {
    247 }
    248 
    249 bool Filter::Token::match(const std::vector<std::string> &rows) const
    250 {
    251   const bool isNot = test(NotFlag);
    252   bool match = false;
    253 
    254   for(const std::string &row : rows) {
    255     if(matchRow(row) ^ isNot)
    256       match = true;
    257     else if(isNot)
    258       return false;
    259   }
    260 
    261   return match;
    262 }
    263 
    264 bool Filter::Token::matchRow(const std::string &str) const
    265 {
    266   const size_t pos = str.find(m_buf);
    267 
    268   if(pos == std::string::npos)
    269     return false;
    270 
    271   const bool isStart = pos == 0, isEnd = pos + m_buf.size() == str.size();
    272 
    273   if(test(StartAnchorFlag) && !isStart)
    274     return false;
    275   if(test(EndAnchorFlag) && !isEnd)
    276     return false;
    277   if(test(FullWordFlag)) {
    278     return
    279       (isStart || !isalnum(str[pos - 1])) &&
    280       (isEnd || !isalnum(str[pos + m_buf.size()]));
    281   }
    282 
    283   return true;
    284 }