1 #include "regexmanager.h"
2
3 #include <cstring>
4 #include <iostream>
5 #include <stack>
6
7 #include "config.h"
8 #include "confighandlerexception.h"
9 #include "configparser.h"
10 #include "logger.h"
11 #include "strprintf.h"
12 #include "utils.h"
13
14 namespace newsboat {
15
RegexManager()16 RegexManager::RegexManager()
17 {
18 // this creates the entries in the map. we need them there to have the
19 // "all" location work.
20 locations["article"];
21 locations["articlelist"];
22 locations["feedlist"];
23 }
24
dump_config(std::vector<std::string> & config_output) const25 void RegexManager::dump_config(std::vector<std::string>& config_output) const
26 {
27 for (const auto& foo : cheat_store_for_dump_config) {
28 config_output.push_back(foo);
29 }
30 }
31
handle_action(const std::string & action,const std::vector<std::string> & params)32 void RegexManager::handle_action(const std::string& action,
33 const std::vector<std::string>& params)
34 {
35 if (action == "highlight") {
36 handle_highlight_action(params);
37 } else if (action == "highlight-article") {
38 handle_highlight_article_action(params);
39 } else {
40 throw ConfigHandlerException(
41 ActionHandlerStatus::INVALID_COMMAND);
42 }
43 std::string line = action;
44 for (const auto& param : params) {
45 line.append(" ");
46 line.append(utils::quote(param));
47 }
48 cheat_store_for_dump_config.push_back(line);
49 }
50
article_matches(Matchable * item)51 int RegexManager::article_matches(Matchable* item)
52 {
53 for (const auto& Matcher : matchers) {
54 if (Matcher.first->matches(item)) {
55 return Matcher.second;
56 }
57 }
58 return -1;
59 }
60
remove_last_regex(const std::string & location)61 void RegexManager::remove_last_regex(const std::string& location)
62 {
63 auto& regexes = locations[location];
64 if (regexes.empty()) {
65 return;
66 }
67
68 regexes.pop_back();
69 }
70
extract_style_tags(std::string & str)71 std::map<size_t, std::string> RegexManager::extract_style_tags(std::string& str)
72 {
73 std::map<size_t, std::string> tags;
74
75 size_t pos = 0;
76 while (pos < str.size()) {
77 auto tag_start = str.find_first_of("<>", pos);
78 if (tag_start == std::string::npos) {
79 break;
80 }
81 if (str[tag_start] == '>') {
82 // Keep unmatched '>' (stfl way of encoding a literal '>')
83 pos = tag_start + 1;
84 continue;
85 }
86 auto tag_end = str.find_first_of("<>", tag_start + 1);
87 if (tag_end == std::string::npos) {
88 break;
89 }
90 if (str[tag_end] == '<') {
91 // First '<' bracket is unmatched, ignoring it
92 pos = tag_start + 1;
93 continue;
94 }
95 if (tag_end - tag_start == 1) {
96 // Convert "<>" into "<" (stfl way of encoding a literal '<')
97 str.erase(tag_end, 1);
98 pos = tag_start + 1;
99 continue;
100 }
101 tags[tag_start] = str.substr(tag_start, tag_end - tag_start + 1);
102 str.erase(tag_start, tag_end - tag_start + 1);
103 pos = tag_start;
104 }
105 return tags;
106 }
107
insert_style_tags(std::string & str,std::map<size_t,std::string> & tags)108 void RegexManager::insert_style_tags(std::string& str,
109 std::map<size_t, std::string>& tags)
110 {
111 // Expand "<" into "<>" (reverse of what happened in extract_style_tags()
112 size_t pos = 0;
113 while (pos < str.size()) {
114 auto bracket = str.find_first_of("<", pos);
115 if (bracket == std::string::npos) {
116 break;
117 }
118 pos = bracket + 1;
119 // Add to strings in the `tags` map so we don't have to shift all the positions in that map
120 // (would be necessary if inserting directly into `str`
121 tags[pos] = ">" + tags[pos];
122 }
123
124 for (auto it = tags.rbegin(); it != tags.rend(); ++it) {
125 if (it->first > str.length()) {
126 // Ignore tags outside of string
127 continue;
128 }
129 str.insert(it->first, it->second);
130 }
131 }
132
merge_style_tag(std::map<size_t,std::string> & tags,const std::string & tag,size_t start,size_t end)133 void RegexManager::merge_style_tag(std::map<size_t, std::string>& tags,
134 const std::string& tag, size_t start, size_t end)
135 {
136 if (end <= start) {
137 return;
138 }
139
140 // Find the latest tag occurring before `end`.
141 // It is important that looping executes in ascending order of location.
142 std::string latest_tag = "</>";
143 for (const auto& location_tag : tags) {
144 size_t location = location_tag.first;
145 if (location > end) {
146 break;
147 }
148 latest_tag = location_tag.second;
149 }
150 tags[start] = tag;
151 tags[end] = latest_tag;
152
153 // Remove any old tags between the start and end marker
154 for (auto it = tags.begin(); it != tags.end(); ) {
155 if (it->first > start && it->first < end) {
156 it = tags.erase(it);
157 } else {
158 ++it;
159 }
160 }
161 }
162
quote_and_highlight(std::string & str,const std::string & location)163 void RegexManager::quote_and_highlight(std::string& str,
164 const std::string& location)
165 {
166 auto& regexes = locations[location];
167
168 auto tag_locations = extract_style_tags(str);
169
170 for (unsigned int i = 0; i < regexes.size(); ++i) {
171 const auto& regex = regexes[i].first;
172 if (regex == nullptr) {
173 continue;
174 }
175 unsigned int offset = 0;
176 int eflags = 0;
177 while (offset < str.length()) {
178 const auto matches = regex->matches(str.substr(offset), 1, eflags);
179 eflags |= REG_NOTBOL; // Don't match beginning-of-line operator (^) in following checks
180 if (matches.empty()) {
181 break;
182 }
183 const auto& match = matches[0];
184 if (match.first != match.second) {
185 const std::string marker = strprintf::fmt("<%u>", i);
186 const int match_start = offset + match.first;
187 const int match_end = offset + match.second;
188 merge_style_tag(tag_locations, marker, match_start, match_end);
189 offset = match_end;
190 } else {
191 offset++;
192 }
193 }
194 }
195
196 insert_style_tags(str, tag_locations);
197 }
198
handle_highlight_action(const std::vector<std::string> & params)199 void RegexManager::handle_highlight_action(const std::vector<std::string>&
200 params)
201 {
202 if (params.size() < 3) {
203 throw ConfigHandlerException(ActionHandlerStatus::TOO_FEW_PARAMS);
204 }
205
206 std::string location = params[0];
207 if (location != "all" && location != "article" &&
208 location != "articlelist" && location != "feedlist") {
209 throw ConfigHandlerException(strprintf::fmt(
210 _("`%s' is an invalid dialog type"), location));
211 }
212
213 std::string errorMessage;
214 auto regex = Regex::compile(params[1], REG_EXTENDED | REG_ICASE, errorMessage);
215 if (regex == nullptr) {
216 throw ConfigHandlerException(strprintf::fmt(
217 _("`%s' is not a valid regular expression: %s"),
218 params[1],
219 errorMessage));
220 }
221 std::string colorstr;
222 if (params[2] != "default") {
223 colorstr.append("fg=");
224 if (!utils::is_valid_color(params[2])) {
225 throw ConfigHandlerException(strprintf::fmt(
226 _("`%s' is not a valid color"),
227 params[2]));
228 }
229 colorstr.append(params[2]);
230 }
231 if (params.size() > 3) {
232 if (params[3] != "default") {
233 if (colorstr.length() > 0) {
234 colorstr.append(",");
235 }
236 colorstr.append("bg=");
237 if (!utils::is_valid_color(params[3])) {
238 throw ConfigHandlerException(
239 strprintf::fmt(
240 _("`%s' is not a valid "
241 "color"),
242 params[3]));
243 }
244 colorstr.append(params[3]);
245 }
246 for (unsigned int i = 4; i < params.size(); ++i) {
247 if (params[i] != "default") {
248 if (!colorstr.empty()) {
249 colorstr.append(",");
250 }
251 colorstr.append("attr=");
252 if (!utils::is_valid_attribute(
253 params[i])) {
254 throw ConfigHandlerException(
255 strprintf::fmt(
256 _("`%s' is not "
257 "a valid "
258 "attribute"),
259 params[i]));
260 }
261 colorstr.append(params[i]);
262 }
263 }
264 }
265 if (location != "all") {
266 LOG(Level::DEBUG,
267 "RegexManager::handle_action: adding rx = %s "
268 "colorstr = %s to location %s",
269 params[1],
270 colorstr,
271 location);
272 locations[location].push_back({std::move(regex), colorstr});
273 } else {
274 std::shared_ptr<Regex> sharedRegex(std::move(regex));
275 for (auto& location : locations) {
276 LOG(Level::DEBUG,
277 "RegexManager::handle_action: adding "
278 "rx = "
279 "%s colorstr = %s to location %s",
280 params[1],
281 colorstr,
282 location.first);
283 location.second.push_back({sharedRegex, colorstr});
284 }
285 }
286 }
287
handle_highlight_article_action(const std::vector<std::string> & params)288 void RegexManager::handle_highlight_article_action(const
289 std::vector<std::string>& params)
290 {
291 if (params.size() < 3) {
292 throw ConfigHandlerException(ActionHandlerStatus::TOO_FEW_PARAMS);
293 }
294
295 std::string expr = params[0];
296 std::string fgcolor = params[1];
297 std::string bgcolor = params[2];
298
299 std::string colorstr;
300 if (fgcolor != "default") {
301 colorstr.append("fg=");
302 if (!utils::is_valid_color(fgcolor)) {
303 throw ConfigHandlerException(strprintf::fmt(
304 _("`%s' is not a valid color"),
305 fgcolor));
306 }
307 colorstr.append(fgcolor);
308 }
309 if (bgcolor != "default") {
310 if (!colorstr.empty()) {
311 colorstr.append(",");
312 }
313 colorstr.append("bg=");
314 if (!utils::is_valid_color(bgcolor)) {
315 throw ConfigHandlerException(strprintf::fmt(
316 _("`%s' is not a valid color"),
317 bgcolor));
318 }
319 colorstr.append(bgcolor);
320 }
321
322 for (unsigned int i = 3; i < params.size(); i++) {
323 if (params[i] != "default") {
324 if (!colorstr.empty()) {
325 colorstr.append(",");
326 }
327 colorstr.append("attr=");
328 if (!utils::is_valid_attribute(params[i])) {
329 throw ConfigHandlerException(
330 strprintf::fmt(
331 _("`%s' is not a valid "
332 "attribute"),
333 params[i]));
334 }
335 colorstr.append(params[i]);
336 }
337 }
338
339 std::shared_ptr<Matcher> m(new Matcher());
340 if (!m->parse(params[0])) {
341 throw ConfigHandlerException(strprintf::fmt(
342 _("couldn't parse filter expression `%s': %s"),
343 params[0],
344 m->get_parse_error()));
345 }
346
347 int pos = locations["articlelist"].size();
348
349 locations["articlelist"].push_back({nullptr, colorstr});
350
351 matchers.push_back(
352 std::pair<std::shared_ptr<Matcher>, int>(m, pos));
353 }
354
get_attrs_stfl_string(const std::string & location,bool hasFocus)355 std::string RegexManager::get_attrs_stfl_string(const std::string& location,
356 bool hasFocus)
357 {
358 const auto& attributes = locations[location];
359 std::string attrstr;
360 for (unsigned int i = 0; i < attributes.size(); ++i) {
361 const std::string& attribute = attributes[i].second;
362 attrstr.append(strprintf::fmt("@style_%u_normal:%s ", i, attribute));
363 if (hasFocus) {
364 attrstr.append(strprintf::fmt("@style_%u_focus:%s ", i, attribute));
365 }
366 }
367 return attrstr;
368 }
369
370 } // namespace newsboat
371