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