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