1 #include "regexmanager.h"
2 
3 #include "3rd-party/catch.hpp"
4 
5 #include "confighandlerexception.h"
6 #include "matchable.h"
7 
8 using namespace newsboat;
9 
10 TEST_CASE("RegexManager throws on invalid command", "[RegexManager]")
11 {
12 	RegexManager rxman;
13 	std::vector<std::string> params;
14 
15 	SECTION("on invalid command") {
16 		REQUIRE_THROWS_AS(
17 			rxman.handle_action("an-invalid-command", params),
18 			ConfigHandlerException);
19 	}
20 }
21 
22 TEST_CASE("RegexManager throws on invalid `highlight' definition",
23 	"[RegexManager]")
24 {
25 	RegexManager rxman;
26 	std::vector<std::string> params;
27 
28 	SECTION("on `highlight' without parameters") {
29 		REQUIRE_THROWS_AS(rxman.handle_action("highlight", params),
30 			ConfigHandlerException);
31 	}
32 
33 	SECTION("on invalid location") {
34 		params = {"invalidloc", "foo", "blue", "red"};
35 		REQUIRE_THROWS_AS(rxman.handle_action("highlight", params),
36 			ConfigHandlerException);
37 	}
38 
39 	SECTION("on invalid regex") {
40 		params = {"feedlist", "*", "blue", "red"};
41 		REQUIRE_THROWS_AS(rxman.handle_action("highlight", params),
42 			ConfigHandlerException);
43 	}
44 }
45 
46 TEST_CASE("RegexManager doesn't throw on valid `highlight' definition",
47 	"[RegexManager]")
48 {
49 	RegexManager rxman;
50 	std::vector<std::string> params;
51 
52 	params = {"articlelist", "foo", "blue", "red"};
53 	REQUIRE_NOTHROW(rxman.handle_action("highlight", params));
54 
55 	params = {"feedlist", "foo", "blue", "red"};
56 	REQUIRE_NOTHROW(rxman.handle_action("highlight", params));
57 
58 	params = {"feedlist", "fbo", "blue", "red", "bold", "underline"};
59 	REQUIRE_NOTHROW(rxman.handle_action("highlight", params));
60 
61 	params = {"all", "fba", "blue", "red", "bold", "underline"};
62 	REQUIRE_NOTHROW(rxman.handle_action("highlight", params));
63 }
64 
65 TEST_CASE("RegexManager highlights according to definition", "[RegexManager]")
66 {
67 	RegexManager rxman;
68 	std::string input;
69 
70 	SECTION("In articlelist") {
71 		rxman.handle_action("highlight", {"articlelist", "foo", "blue", "red"});
72 		input = "xfoox";
73 		rxman.quote_and_highlight(input, "articlelist");
74 		REQUIRE(input == "x<0>foo</>x");
75 	}
76 
77 	SECTION("In feedlist") {
78 		rxman.handle_action("highlight", {"feedlist", "foo", "blue", "red"});
79 		input = "yfooy";
80 		rxman.quote_and_highlight(input, "feedlist");
81 		REQUIRE(input == "y<0>foo</>y");
82 	}
83 }
84 
85 TEST_CASE("quote_and_highlight() only matches `^` at the start of the line",
86 	"[RegexManager]")
87 {
88 	RegexManager rxman;
89 	std::string input = "This is a test";
90 
91 	SECTION("^.") {
92 		rxman.handle_action("highlight", {"article", "^.", "blue", "red"});
93 		rxman.quote_and_highlight(input, "article");
94 		REQUIRE(input == "<0>T</>his is a test");
95 	}
96 
97 	SECTION("(^Th|^is)") {
98 		rxman.handle_action("highlight", {"article", "(^Th|^is)", "blue", "red"});
99 		rxman.quote_and_highlight(input, "article");
100 		REQUIRE(input == "<0>Th</>is is a test");
101 	}
102 }
103 
104 // This test might seem totally out of the blue, but we do need it: as of this
105 // writing, `highlight-article` add a nullptr for a regex to "articlelist"
106 // context, which would've crashed the program if it were to be dereferenced.
107 // This test checks that those nullptrs are skipped.
108 TEST_CASE("RegexManager::quote_and_highlight works fine even if there were "
109 	"`highlight-article` commands",
110 	"[RegexManager]")
111 {
112 	RegexManager rxman;
113 
114 	rxman.handle_action("highlight", {"articlelist", "foo", "blue", "red"});
115 	rxman.handle_action("highlight-article", {"title==\"\"", "blue", "red"});
116 
117 	std::string input = "xfoox";
118 	rxman.quote_and_highlight(input, "articlelist");
119 	REQUIRE(input == "x<0>foo</>x");
120 }
121 
122 TEST_CASE("RegexManager preserves text when there's nothing to highlight",
123 	"[RegexManager]")
124 {
125 	RegexManager rxman;
126 	std::string input = "xbarx";
127 	rxman.quote_and_highlight(input, "feedlist");
128 	REQUIRE(input == "xbarx");
129 
130 	input = "a<b>";
131 	rxman.quote_and_highlight(input, "feedlist");
132 	REQUIRE(input == "a<b>");
133 
134 	SECTION("encode `<` as `<>` for stfl") {
135 		input = "<";
136 		rxman.quote_and_highlight(input, "feedlist");
137 		REQUIRE(input == "<>");
138 	}
139 }
140 
141 TEST_CASE("`highlight all` adds rules for all locations", "[RegexManager]")
142 {
143 	RegexManager rxman;
144 	std::vector<std::string> params = {"all", "foo", "red"};
145 	REQUIRE_NOTHROW(rxman.handle_action("highlight", params));
146 	std::string input = "xxfooyy";
147 
148 	for (auto location : {
149 			"article", "articlelist", "feedlist"
150 		}) {
SECTION(location)151 		SECTION(location) {
152 			rxman.quote_and_highlight(input, location);
153 			REQUIRE(input == "xx<0>foo</>yy");
154 		}
155 	}
156 }
157 
158 TEST_CASE("RegexManager does not hang on regexes that can match empty strings",
159 	"[RegexManager]")
160 {
161 	RegexManager rxman;
162 	std::string input = "The quick brown fox jumps over the lazy dog";
163 
164 	rxman.handle_action("highlight", {"feedlist", "w*", "blue", "red"});
165 	rxman.quote_and_highlight(input, "feedlist");
166 	REQUIRE(input == "The quick bro<0>w</>n fox jumps over the lazy dog");
167 }
168 
169 TEST_CASE("RegexManager does not hang on regexes that match empty strings",
170 	"[RegexManager]")
171 {
172 	RegexManager rxman;
173 	std::string input = "The quick brown fox jumps over the lazy dog";
174 	const std::string compare = input;
175 
176 	SECTION("testing end of line empty.") {
177 		rxman.handle_action("highlight", {"feedlist", "$", "blue", "red"});
178 		rxman.quote_and_highlight(input, "feedlist");
179 		REQUIRE(input == compare);
180 	}
181 
182 	SECTION("testing beginning of line empty") {
183 		rxman.handle_action("highlight", {"feedlist", "^", "blue", "red"});
184 		rxman.quote_and_highlight(input, "feedlist");
185 		REQUIRE(input == compare);
186 	}
187 
188 	SECTION("testing empty line") {
189 		rxman.handle_action("highlight", {"feedlist", "^$", "blue", "red"});
190 		rxman.quote_and_highlight(input, "feedlist");
191 		REQUIRE(input == compare);
192 	}
193 }
194 
195 TEST_CASE("quote_and_highlight wraps highlighted text in numbered tags",
196 	"[RegexManager]")
197 {
198 	RegexManager rxman;
199 	std::string input =  "The quick brown fox jumps over the lazy dog";
200 
201 	SECTION("Beginning of line match first") {
202 		const std::string output =
203 			"<0>The</> quick <1>brown</> fox jumps over <0>the</> lazy dog";
204 		rxman.handle_action("highlight", {"article", "the", "red"});
205 		rxman.handle_action("highlight", {"article", "brown", "blue"});
206 		rxman.quote_and_highlight(input, "article");
207 		REQUIRE(input == output);
208 	}
209 
210 	SECTION("Beginning of line match second") {
211 		const std::string output =
212 			"<1>The</> quick <0>brown</> fox jumps over <1>the</> lazy dog";
213 		rxman.handle_action("highlight", {"article", "brown", "blue"});
214 		rxman.handle_action("highlight", {"article", "the", "red"});
215 		rxman.quote_and_highlight(input, "article");
216 		REQUIRE(input == output);
217 	}
218 
219 	SECTION("2 non-overlapping highlights") {
220 		const std::string output =
221 			"The <0>quick</> <1>brown</> fox jumps over the lazy dog";
222 		rxman.handle_action("highlight", {"article", "quick", "red"});
223 		rxman.handle_action("highlight", {"article", "brown", "blue"});
224 		rxman.quote_and_highlight(input, "article");
225 		REQUIRE(input == output);
226 	}
227 }
228 
229 TEST_CASE("RegexManager::dump_config turns each `highlight` and "
230 	"`highlight-article` rule into a string, and appends them onto a vector",
231 	"[RegexManager]")
232 {
233 	RegexManager rxman;
234 	std::vector<std::string> result;
235 
236 	SECTION("Empty object returns empty vector") {
237 		REQUIRE_NOTHROW(rxman.dump_config(result));
238 		REQUIRE(result.empty());
239 	}
240 
241 	SECTION("One rule") {
242 		rxman.handle_action("highlight", {"article", "this test is", "green"});
243 		REQUIRE_NOTHROW(rxman.dump_config(result));
244 		REQUIRE(result.size() == 1);
245 		REQUIRE(result[0] == R"#(highlight "article" "this test is" "green")#");
246 	}
247 
248 	SECTION("Two rules, one of them a `highlight-article`") {
249 		rxman.handle_action("highlight", {"all", "keywords", "red", "blue"});
250 		rxman.handle_action(
251 			"highlight-article",
252 		{"title==\"\"", "green", "black"});
253 		REQUIRE_NOTHROW(rxman.dump_config(result));
254 		REQUIRE(result.size() == 2);
255 		REQUIRE(result[0] == R"#(highlight "all" "keywords" "red" "blue")#");
256 		REQUIRE(result[1] == R"#(highlight-article "title==\"\"" "green" "black")#");
257 	}
258 }
259 
260 TEST_CASE("RegexManager::dump_config appends to given vector", "[RegexManager]")
261 {
262 	RegexManager rxman;
263 	std::vector<std::string> result;
264 
265 	result.emplace_back("sentinel");
266 
267 	rxman.handle_action("highlight", {"all", "this", "black"});
268 
269 	REQUIRE_NOTHROW(rxman.dump_config(result));
270 	REQUIRE(result.size() == 2);
271 	REQUIRE(result[0] == "sentinel");
272 	REQUIRE(result[1] == R"#(highlight "all" "this" "black")#");
273 }
274 
275 TEST_CASE("RegexManager::handle_action throws ConfigHandlerException "
276 	"on invalid foreground color",
277 	"[RegexManager]")
278 {
279 	RegexManager rxman;
280 
281 	REQUIRE_THROWS_AS(
282 		rxman.handle_action("highlight", {"all", "keyword", "whatever"}),
283 		ConfigHandlerException);
284 
285 	REQUIRE_THROWS_AS(
286 		rxman.handle_action("highlight", {"feedlist", "keyword", ""}),
287 		ConfigHandlerException);
288 
289 	REQUIRE_THROWS_AS(
290 		rxman.handle_action(
291 			"highlight-article",
292 	{"author == \"\"", "whatever", "white"}),
293 	ConfigHandlerException);
294 
295 	REQUIRE_THROWS_AS(
296 		rxman.handle_action(
297 			"highlight-article",
298 	{"title =~ \"k\"", "", "white"}),
299 	ConfigHandlerException);
300 }
301 
302 TEST_CASE("RegexManager::handle_action throws ConfigHandlerException "
303 	"on invalid background color",
304 	"[RegexManager]")
305 {
306 	RegexManager rxman;
307 
308 	REQUIRE_THROWS_AS(
309 		rxman.handle_action(
310 			"highlight",
311 	{"all", "keyword", "red", "whatever"}),
312 	ConfigHandlerException);
313 
314 	REQUIRE_THROWS_AS(
315 		rxman.handle_action(
316 			"highlight",
317 	{"feedlist", "keyword", "green", ""}),
318 	ConfigHandlerException);
319 
320 	REQUIRE_THROWS_AS(
321 		rxman.handle_action(
322 			"highlight-article",
323 	{"title == \"keyword\"", "red", "whatever"}),
324 	ConfigHandlerException);
325 
326 	REQUIRE_THROWS_AS(
327 		rxman.handle_action(
328 			"highlight-article",
329 	{"content == \"\"", "green", ""}),
330 	ConfigHandlerException);
331 }
332 
333 TEST_CASE("RegexManager::handle_action throws ConfigHandlerException "
334 	"on invalid attribute",
335 	"[RegexManager]")
336 {
337 	RegexManager rxman;
338 
339 	REQUIRE_THROWS_AS(
340 		rxman.handle_action(
341 			"highlight",
342 	{"all", "keyword", "red", "green", "sparkles"}),
343 	ConfigHandlerException);
344 
345 	REQUIRE_THROWS_AS(
346 		rxman.handle_action(
347 			"highlight",
348 	{"feedlist", "keyword", "green", "red", ""}),
349 	ConfigHandlerException);
350 
351 	REQUIRE_THROWS_AS(
352 		rxman.handle_action(
353 			"highlight-article",
354 	{"title==\"\"", "red", "green", "sparkles"}),
355 	ConfigHandlerException);
356 
357 	REQUIRE_THROWS_AS(
358 		rxman.handle_action(
359 			"highlight-article",
360 	{"title==\"\"", "green", "red", ""}),
361 	ConfigHandlerException);
362 }
363 
364 TEST_CASE("RegexManager throws on invalid `highlight-article' definition",
365 	"[RegexManager]")
366 {
367 	RegexManager rxman;
368 	std::vector<std::string> params;
369 
370 	SECTION("on `highlight-article' without parameters") {
371 		REQUIRE_THROWS_AS(rxman.handle_action("highlight-article", params),
372 			ConfigHandlerException);
373 	}
374 
375 	SECTION("on invalid filter expression") {
376 		params = {"a = b", "red", "green"};
377 		REQUIRE_THROWS_AS(rxman.handle_action("highlight-article", params),
378 			ConfigHandlerException);
379 	}
380 
381 	SECTION("on missing colors") {
382 		params = {"title==\"\""};
383 		REQUIRE_THROWS_AS(rxman.handle_action("highlight-article", params),
384 			ConfigHandlerException);
385 	}
386 
387 	SECTION("on missing background color") {
388 		params = {"title==\"\"", "white"};
389 		REQUIRE_THROWS_AS(rxman.handle_action("highlight-article", params),
390 			ConfigHandlerException);
391 	}
392 }
393 
394 TEST_CASE("RegexManager doesn't throw on valid `highlight-article' definition",
395 	"[RegexManager]")
396 {
397 	RegexManager rxman;
398 	std::vector<std::string> params;
399 
400 	params = {"title == \"\"", "blue", "red"};
401 	REQUIRE_NOTHROW(rxman.handle_action("highlight-article", params));
402 
403 	params = {"content =~ \"keyword\"", "blue", "red"};
404 	REQUIRE_NOTHROW(rxman.handle_action("highlight-article", params));
405 
406 	params = {"unread == \"yes\"", "blue", "red", "bold", "underline"};
407 	REQUIRE_NOTHROW(rxman.handle_action("highlight-article", params));
408 
409 	params = {"age > 3", "blue", "red", "bold", "underline"};
410 	REQUIRE_NOTHROW(rxman.handle_action("highlight-article", params));
411 }
412 
413 struct RegexManagerMockMatchable : public Matchable {
414 public:
attribute_valueRegexManagerMockMatchable415 	nonstd::optional<std::string> attribute_value(const std::string& attribname)
416 	const override
417 	{
418 		if (attribname == "attr") {
419 			return "val";
420 		}
421 		return nonstd::nullopt;
422 	}
423 };
424 
425 TEST_CASE("RegexManager::article_matches returns position of the Matcher "
426 	"that matches a given Matchable",
427 	"[RegexManager]")
428 {
429 	RegexManager rxman;
430 	RegexManagerMockMatchable mock;
431 
432 	const auto cmd = std::string("highlight-article");
433 
434 	SECTION("Just one rule") {
435 		rxman.handle_action(cmd, {"attr != \"hello\"", "red", "green"});
436 
437 		REQUIRE(rxman.article_matches(&mock) == 0);
438 	}
439 
440 	SECTION("Couple rules") {
441 		rxman.handle_action(cmd, {"attr != \"val\"", "green", "white"});
442 		rxman.handle_action(cmd, {"attr # \"entry\"", "green", "white"});
443 		rxman.handle_action(cmd, {"attr == \"val\"", "green", "white"});
444 		rxman.handle_action(cmd, {"attr =~ \"hello\"", "green", "white"});
445 
446 		REQUIRE(rxman.article_matches(&mock) == 2);
447 	}
448 }
449 
450 TEST_CASE("RegexManager::article_matches returns -1 if there are no Matcher "
451 	"to match a given Matchable",
452 	"[RegexManager]")
453 {
454 	RegexManager rxman;
455 	RegexManagerMockMatchable mock;
456 
457 	const auto cmd = std::string("highlight-article");
458 
459 	SECTION("No rules") {
460 		REQUIRE(rxman.article_matches(&mock) == -1);
461 	}
462 
463 	SECTION("Just one rule") {
464 		rxman.handle_action(cmd, {"attr == \"hello\"", "red", "green"});
465 
466 		REQUIRE(rxman.article_matches(&mock) == -1);
467 	}
468 
469 	SECTION("Couple rules") {
470 		rxman.handle_action(cmd, {"attr != \"val\"", "green", "white"});
471 		rxman.handle_action(cmd, {"attr # \"entry\"", "green", "white"});
472 		rxman.handle_action(cmd, {"attr =~ \"hello\"", "green", "white"});
473 
474 		REQUIRE(rxman.article_matches(&mock) == -1);
475 	}
476 }
477 
478 TEST_CASE("RegexManager::remove_last_regex removes last added `highlight` rule",
479 	"[RegexManager]")
480 {
481 	RegexManager rxman;
482 
483 	rxman.handle_action("highlight", {"articlelist", "foo", "blue", "red"});
484 	rxman.handle_action("highlight", {"articlelist", "bar", "blue", "red"});
485 
486 	const auto INPUT = std::string("xfoobarx");
487 
488 	auto input = INPUT;
489 	rxman.quote_and_highlight(input, "articlelist");
490 	REQUIRE(input == "x<0>foo<1>bar</>x");
491 
492 	input = INPUT;
493 	rxman.quote_and_highlight(input, "feedlist");
494 	REQUIRE(input == INPUT);
495 
496 	REQUIRE_NOTHROW(rxman.remove_last_regex("articlelist"));
497 
498 	input = INPUT;
499 	rxman.quote_and_highlight(input, "articlelist");
500 	REQUIRE(input == "x<0>foo</>barx");
501 
502 	input = INPUT;
503 	rxman.quote_and_highlight(input, "feedlist");
504 	REQUIRE(input == INPUT);
505 }
506 
507 TEST_CASE("RegexManager::remove_last_regex does not crash if there are "
508 	"no regexes to remove",
509 	"[RegexManager]")
510 {
511 	RegexManager rxman;
512 
513 	SECTION("No rules existed") {
514 		rxman.remove_last_regex("articlelist");
515 
516 		SECTION("Repeated calls don't crash either") {
517 			rxman.remove_last_regex("articlelist");
518 			rxman.remove_last_regex("articlelist");
519 			rxman.remove_last_regex("articlelist");
520 
521 			rxman.remove_last_regex("feedlist");
522 		}
523 	}
524 
525 	SECTION("A few rules were added and then deleted") {
526 		rxman.handle_action("highlight", {"articlelist", "test test", "red"});
527 		rxman.handle_action("highlight", {"articlelist", "another test", "red"});
528 		rxman.handle_action("highlight", {"articlelist", "more", "green", "blue"});
529 
530 		rxman.remove_last_regex("articlelist");
531 		rxman.remove_last_regex("articlelist");
532 		rxman.remove_last_regex("articlelist");
533 		// At this point, all the rules are removed
534 		rxman.remove_last_regex("articlelist");
535 
536 		SECTION("Repeated calls don't crash either") {
537 			rxman.remove_last_regex("articlelist");
538 			rxman.remove_last_regex("articlelist");
539 			rxman.remove_last_regex("articlelist");
540 
541 			rxman.remove_last_regex("feedlist");
542 		}
543 	}
544 }
545 
546 TEST_CASE("RegexManager uses POSIX extended regex syntax",
547 	"[RegexManager]")
548 {
549 	// This syntax is documented in The Open Group Base Specifications Issue 7,
550 	// IEEE Std 1003.1-2008 Section 9, "Regular Expressions":
551 	// https://pubs.opengroup.org/onlinepubs/9699919799.2008edition/basedefs/V1_chap09.html
552 
553 	// Since POSIX extended regular expressions are pretty basic, it's hard to
554 	// find stuff that they support but other engines don't. So in order to
555 	// ensure that we're using EREs, these tests try stuff that's *not*
556 	// supported by EREs.
557 	//
558 	// Ideas gleaned from https://www.regular-expressions.info/refcharacters.html
559 
560 	RegexManager rxman;
561 
562 	// Supported by Perl, PCRE, PHP and others
563 	SECTION("No support for escape sequence") {
564 		rxman.handle_action("highlight", {"articlelist", R"#(\Q*]+\E)#", "red"});
565 
566 		std::string input = "*]+";
567 		rxman.quote_and_highlight(input, "articlelist");
568 		REQUIRE(input == "*]+");
569 	}
570 
571 	SECTION("No support for hexadecimal escape") {
572 		rxman.handle_action("highlight", {"articlelist", R"#(^va\x6Cue)#", "red"});
573 
574 		std::string input = "value";
575 		rxman.quote_and_highlight(input, "articlelist");
576 		REQUIRE(input == "value");
577 	}
578 
579 	SECTION("No support for \\a as alert/bell control character") {
580 		rxman.handle_action("highlight", {"articlelist", R"#(\a)#", "red"});
581 
582 		std::string input = "\x07";
583 		rxman.quote_and_highlight(input, "articlelist");
584 		REQUIRE(input == "\x07");
585 	}
586 
587 	SECTION("No support for \\b as backspace control character") {
588 		rxman.handle_action("highlight", {"articlelist", R"#(\b)#", "red"});
589 
590 		std::string input = "\x08";
591 		rxman.quote_and_highlight(input, "articlelist");
592 		REQUIRE(input == "\x08");
593 	}
594 
595 	// If you add more checks to this test, consider adding the same to Matcher tests
596 }
597 
598 TEST_CASE("quote_and_highlight() does not break existing tags like <unread>",
599 	"[RegexManager]")
600 {
601 	RegexManager rxman;
602 	std::string input =  "<unread>This entry is unread</>";
603 
604 	WHEN("matching `read`") {
605 		const std::string output = "<unread>This entry is un<0>read</>";
606 		rxman.handle_action("highlight", {"article", "read", "red"});
607 		rxman.quote_and_highlight(input, "article");
608 		REQUIRE(input == output);
609 	}
610 
611 	WHEN("matching `unread`") {
612 		const std::string output = "<unread>This entry is <0>unread</>";
613 		rxman.handle_action("highlight", {"article", "unread", "red"});
614 		rxman.quote_and_highlight(input, "article");
615 		REQUIRE(input == output);
616 	}
617 
618 	WHEN("matching the full line") {
619 		rxman.handle_action("highlight", {"article", "^.*$", "red"});
620 
621 		THEN("the <unread> tag is overwritten") {
622 			const std::string output = "<0>This entry is unread</>";
623 			rxman.quote_and_highlight(input, "article");
624 			REQUIRE(input == output);
625 		}
626 	}
627 }
628 
629 TEST_CASE("quote_and_highlight() ignores tags when matching the regular expressions",
630 	"[RegexManager]")
631 {
632 	RegexManager rxman;
633 	std::string input =  "<unread>This entry is unread</>";
634 
635 	WHEN("matching text at the start of the line") {
636 		rxman.handle_action("highlight", {"article", "^This", "red"});
637 
638 		THEN("the <unread> tag should be ignored") {
639 			const std::string output = "<0>This<unread> entry is unread</>";
640 			rxman.quote_and_highlight(input, "article");
641 			REQUIRE(input == output);
642 		}
643 	}
644 
645 	WHEN("matching text at the end of the line") {
646 		rxman.handle_action("highlight", {"article", "unread$", "red"});
647 
648 		THEN("the closing tag `</>` (related to <unread>) should be ignored") {
649 			const std::string output = "<unread>This entry is <0>unread</>";
650 			rxman.quote_and_highlight(input, "article");
651 			REQUIRE(input == output);
652 		}
653 	}
654 
655 	WHEN("matching text which contains `<u>...</>` markers (added by HTML renderer)") {
656 		input = "Some<u>thing</> is underlined";
657 		rxman.handle_action("highlight", {"article", "Something", "red"});
658 
659 		THEN("the tags should be ignored when matching text with a regular expression") {
660 			const std::string output = "<0>Something</> is underlined";
661 			rxman.quote_and_highlight(input, "article");
662 			REQUIRE(input == output);
663 		}
664 	}
665 }
666 
667 TEST_CASE("quote_and_highlight() generates a sensible output when multiple matches overlap",
668 	"[RegexManager]")
669 {
670 	RegexManager rxman;
671 	std::string input = "The quick brown fox jumps over the lazy dog";
672 
673 	WHEN("a second match is completely inside of the first match") {
674 		rxman.handle_action("highlight", {"article", "The quick brown", "red"});
675 		rxman.handle_action("highlight", {"article", "quick", "red"});
676 
677 		THEN("the first style should be restored at the end of the second match") {
678 			const std::string output =
679 				"<0>The <1>quick<0> brown</> fox jumps over the lazy dog";
680 			rxman.quote_and_highlight(input, "article");
681 			REQUIRE(input == output);
682 		}
683 	}
684 
685 	WHEN("a second match completely encloses the first match") {
686 		rxman.handle_action("highlight", {"article", "quick", "red"});
687 		rxman.handle_action("highlight", {"article", "The quick brown", "red"});
688 
689 		THEN("the first style should not be present anymore") {
690 			const std::string output =
691 				"<1>The quick brown</> fox jumps over the lazy dog";
692 			rxman.quote_and_highlight(input, "article");
693 			REQUIRE(input == output);
694 		}
695 	}
696 
697 	WHEN("a second match starts somewhere in the first match") {
698 		rxman.handle_action("highlight", {"article", "quick brown", "red"});
699 		rxman.handle_action("highlight", {"article", "brown fox", "red"});
700 
701 		THEN("the first style should only be overwritten for the part where the matches intersect") {
702 			const std::string output =
703 				"The <0>quick <1>brown fox</> jumps over the lazy dog";
704 			rxman.quote_and_highlight(input, "article");
705 			REQUIRE(input == output);
706 		}
707 	}
708 
709 	WHEN("a second match ends somewhere in the first match") {
710 		rxman.handle_action("highlight", {"article", "brown fox", "red"});
711 		rxman.handle_action("highlight", {"article", "quick brown", "red"});
712 
713 		THEN("the first style should only be overwritten for the part where the matches intersect") {
714 			const std::string output =
715 				"The <1>quick brown<0> fox</> jumps over the lazy dog";
716 			rxman.quote_and_highlight(input, "article");
717 			REQUIRE(input == output);
718 		}
719 	}
720 
721 	WHEN("there are three overlapping matches") {
722 		rxman.handle_action("highlight", {"article", "quick.*dog", "red"});
723 		rxman.handle_action("highlight", {"article", "fox jumps over", "red"});
724 		rxman.handle_action("highlight", {"article", "brown fox", "red"});
725 
726 		THEN("newer matches overwrite older matches") {
727 			const std::string output =
728 				"The <0>quick <2>brown fox<1> jumps over<0> the lazy dog</>";
729 			rxman.quote_and_highlight(input, "article");
730 			REQUIRE(input == output);
731 		}
732 	}
733 }
734 
735 TEST_CASE("quote_and_highlight() keeps stfl-encoded angle brackets and allows matching them directly",
736 	"[RegexManager]")
737 {
738 	RegexManager rxman;
739 	std::string input = "<unread>title with <>literal> angle brackets</>";
740 
741 	SECTION("stfl-encoded angle brackets are kept/restored") {
742 		const std::string output = "<unread>title with <>literal> angle brackets</>";
743 		rxman.quote_and_highlight(input, "article");
744 		REQUIRE(input == output);
745 	}
746 
747 	SECTION("angle brackets can be matched directly") {
748 		const std::string output =
749 			"<unread>title with <0><>literal><unread> angle brackets</>";
750 		rxman.handle_action("highlight", {"article", "<literal>", "red"});
751 		rxman.quote_and_highlight(input, "article");
752 		REQUIRE(input == output);
753 	}
754 }
755 
756 TEST_CASE("extract_style_tags() returns map which links locations to tags from input string",
757 	"[RegexManager]")
758 {
759 	RegexManager rxman;
760 
761 	SECTION("input with no tags at all") {
762 		std::string input = "The quick brown fox jumps over the lazy dog";
763 		const std::string output = "The quick brown fox jumps over the lazy dog";
764 		auto tags = rxman.extract_style_tags(input);
765 		REQUIRE(input == output);
766 		REQUIRE(tags.size() == 0);
767 	}
768 
769 	SECTION("input with various tags") {
770 		std::string input = "<unread>title <0>with <u>underline<0> and style</>";
771 		const std::string output = "title with underline and style";
772 		auto tags = rxman.extract_style_tags(input);
773 		REQUIRE(input == output);
774 		REQUIRE(tags[0]  == "<unread>");
775 		REQUIRE(tags[6]  == "<0>");
776 		REQUIRE(tags[11] == "<u>");
777 		REQUIRE(tags[20] == "<0>");
778 		REQUIRE(tags[30] == "</>");
779 		REQUIRE(tags.size() == 5);
780 	}
781 }
782 
783 TEST_CASE("extract_style_tags() keeps stfl-encoded angle brackets in string",
784 	"[RegexManager]")
785 {
786 	RegexManager rxman;
787 
788 	SECTION("stfl-encoded angle brackets should be preserved") {
789 		std::string input = "<unread>title with <>literal> angle brackets</>";
790 		const std::string output = "title with <literal> angle brackets";
791 		auto tags = rxman.extract_style_tags(input);
792 		REQUIRE(input == output);
793 		REQUIRE(tags[0]  == "<unread>");
794 		REQUIRE(tags[35] == "</>");
795 		REQUIRE(tags.size() == 2);
796 	}
797 }
798 
799 TEST_CASE("extract_style_tags() ignores invalid characters",
800 	"[RegexManager]")
801 {
802 	RegexManager rxman;
803 	// Different output might be acceptable as this input would/should be
804 	// invalid but it makes sense to keep a consistent result when refactoring.
805 
806 	SECTION("double tag open") {
807 		std::string input = "<unread>title <with <u>repeated tag opening bracket</>";
808 		const std::string output = "title <with repeated tag opening bracket";
809 		auto tags = rxman.extract_style_tags(input);
810 		REQUIRE(input == output);
811 		REQUIRE(tags[0]  == "<unread>");
812 		REQUIRE(tags[12] == "<u>");
813 		REQUIRE(tags[40] == "</>");
814 		REQUIRE(tags.size() == 3);
815 	}
816 
817 	SECTION("unmatched '<' at end of line") {
818 		std::string input = "some <u>underlining</>, nothing else<";
819 		const std::string output = "some underlining, nothing else<";
820 		auto tags = rxman.extract_style_tags(input);
821 		REQUIRE(input == output);
822 		REQUIRE(tags[5]  == "<u>");
823 		REQUIRE(tags[16]  == "</>");
824 		REQUIRE(tags.size() == 2);
825 	}
826 }
827 
828 TEST_CASE("insert_style_tags() adds tags into string at correct positions",
829 	"[RegexManager]")
830 {
831 	RegexManager rxman;
832 	std::string input = "This is a sentence";
833 
834 	SECTION("string does not change if no tags are specified") {
835 		std::map<size_t, std::string> tags;
836 		const std::string output = "This is a sentence";
837 		rxman.insert_style_tags(input, tags);
838 		REQUIRE(input == output);
839 	}
840 
841 	SECTION("tags are added at the correct location") {
842 		std::map<size_t, std::string> tags = {
843 			{0, "<0>"},
844 			{1, "<1>"},
845 			{10, "<hello>"},
846 			{18, "</>"},
847 		};
848 		const std::string output = "<0>T<1>his is a <hello>sentence</>";
849 		rxman.insert_style_tags(input, tags);
850 		REQUIRE(input == output);
851 	}
852 }
853 
854 TEST_CASE("insert_style_tags() stfl-encodes angle brackets existing in input string",
855 	"[RegexManager]")
856 {
857 	RegexManager rxman;
858 	std::string input = ">>This <is> a sentence with brackets<<";
859 
860 	SECTION("brackets are stfl-encoded") {
861 		std::map<size_t, std::string> tags;
862 		const std::string output = ">>This <>is> a sentence with brackets<><>";
863 		rxman.insert_style_tags(input, tags);
864 		REQUIRE(input == output);
865 	}
866 
867 	SECTION("brackets are stfl-encoded and tags are inserted") {
868 		std::map<size_t, std::string> tags = {
869 			{0, "<0>"},
870 			{1, "<1>"},
871 			{6, "<hello>"},
872 			{38, "</>"},
873 		};
874 		const std::string output =
875 			"<0>><1>>This<hello> <>is> a sentence with brackets<><></>";
876 		rxman.insert_style_tags(input, tags);
877 		REQUIRE(input == output);
878 	}
879 }
880 
881 TEST_CASE("insert_style_tags() does not crash on invalid input",
882 	"[RegexManager]")
883 {
884 	RegexManager rxman;
885 	std::string input = "test this";
886 
887 	SECTION("tags with a position outside  of the string are ignored") {
888 		std::map<size_t, std::string> tags = {
889 			{0, "<test>"},
890 			{9, "<in>"},
891 			{10, "<out>"},
892 		};
893 		const std::string output = "<test>test this<in>";
894 		rxman.insert_style_tags(input, tags);
895 		REQUIRE(input == output);
896 	}
897 }
898 
899 TEST_CASE("merge_style_tag() removes tags between start and end positions",
900 	"[RegexManager]")
901 {
902 	RegexManager rxman;
903 	const std::string new_tag = "<test>";
904 	std::map<size_t, std::string> tags = {
905 		{0, "<0>"},
906 		{5, "<1>"},
907 		{7, "<2>"},
908 		{10, "<3>"},
909 		{15, "</>"},
910 	};
911 
912 	rxman.merge_style_tag(tags, new_tag, 2, 10);
913 	REQUIRE(tags[0] == "<0>");
914 	REQUIRE(tags[2] == new_tag);
915 	REQUIRE(tags[10] == "<3>");
916 	REQUIRE(tags[15] == "</>");
917 	REQUIRE(tags.size() == 4);
918 }
919 
920 TEST_CASE("merge_style_tag() restores previous tag after the inserted tag if necessary",
921 	"[RegexManager]")
922 {
923 	RegexManager rxman;
924 	const std::string new_tag = "<test>";
925 	std::map<size_t, std::string> tags = {
926 		{0, "<0>"},
927 		{5, "<1>"},
928 		{10, "</>"},
929 	};
930 
931 	SECTION("end before tag switch so restore first tag") {
932 		rxman.merge_style_tag(tags, new_tag, 0, 4);
933 		REQUIRE(tags[0] == new_tag);
934 		REQUIRE(tags[4] == "<0>");
935 		REQUIRE(tags[5] == "<1>");
936 		REQUIRE(tags[10] == "</>");
937 		REQUIRE(tags.size() == 4);
938 	}
939 
940 	SECTION("end on tag switch so no need to restore anything") {
941 		rxman.merge_style_tag(tags, new_tag, 0, 5);
942 		REQUIRE(tags[0] == new_tag);
943 		REQUIRE(tags[5] == "<1>");
944 		REQUIRE(tags[10] == "</>");
945 		REQUIRE(tags.size() == 3);
946 	}
947 
948 	SECTION("end after a closing tag (</>) so close at the end") {
949 		SECTION("on the closing tag") {
950 			rxman.merge_style_tag(tags, new_tag, 0, 10);
951 			REQUIRE(tags[0] == new_tag);
952 			REQUIRE(tags[10] == "</>");
953 			REQUIRE(tags.size() == 2);
954 		}
955 
956 		SECTION("after the closing tag") {
957 			rxman.merge_style_tag(tags, new_tag, 0, 11);
958 			REQUIRE(tags[0] == new_tag);
959 			REQUIRE(tags[11] == "</>");
960 			REQUIRE(tags.size() == 2);
961 		}
962 	}
963 }
964 
965 TEST_CASE("merge_style_tag() does not crash on invalid input",
966 	"[RegexManager]")
967 {
968 	RegexManager rxman;
969 	std::map<size_t, std::string> tags;
970 	const std::string new_tag = "<test>";
971 
972 	SECTION("ignore tag merging if `end <= start`") {
973 		rxman.merge_style_tag(tags, new_tag, 0, 0);
974 		rxman.merge_style_tag(tags, new_tag, 1, 0);
975 		rxman.merge_style_tag(tags, new_tag, 5, 4);
976 		REQUIRE(tags.size() == 0);
977 	}
978 }
979