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 }