mirror of
				https://github.com/catchorg/Catch2.git
				synced 2025-10-31 12:17:11 +01:00 
			
		
		
		
	Handle ANSI escape sequences when performing column wrapping (#2849)
This PR adds functionality to skip around ANSI escape sequences in catch_textflow so they do not contribute to line length and line wrapping code does not split escape sequences in the middle. I've implemented this by creating a AnsiSkippingString abstraction that has a bidirectional iterator that can skip around escape sequences while iterating. Additionally I refactored Column::const_iterator to be iterator-based rather than index-based so this abstraction is a simple drop-in for std::string. Currently only color sequences are handled, other escape sequences are left unaffected. Motivation: Text with ANSI color sequences gets messed up when being output by Catch2 #2833.
This commit is contained in:
		| @@ -12,6 +12,7 @@ | ||||
| #include <sstream> | ||||
|  | ||||
| using Catch::TextFlow::Column; | ||||
| using Catch::TextFlow::AnsiSkippingString; | ||||
|  | ||||
| namespace { | ||||
|     static std::string as_written(Column const& c) { | ||||
| @@ -198,3 +199,202 @@ TEST_CASE( "#1400 - TextFlow::Column wrapping would sometimes duplicate words", | ||||
|             "  in \n" | ||||
|             "  convallis posuere, libero nisi ultricies orci, nec lobortis."); | ||||
| } | ||||
|  | ||||
| TEST_CASE( "TextFlow::AnsiSkippingString skips ansi sequences", | ||||
|            "[TextFlow][ansiskippingstring][approvals]" ) { | ||||
|  | ||||
|     SECTION("basic string") { | ||||
|         std::string text = "a\033[38;2;98;174;239mb\033[38mc\033[0md\033[me"; | ||||
|         AnsiSkippingString str(text); | ||||
|  | ||||
|         SECTION( "iterates forward" ) { | ||||
|             auto it = str.begin(); | ||||
|             CHECK(*it == 'a'); | ||||
|             ++it; | ||||
|             CHECK(*it == 'b'); | ||||
|             ++it; | ||||
|             CHECK(*it == 'c'); | ||||
|             ++it; | ||||
|             CHECK(*it == 'd'); | ||||
|             ++it; | ||||
|             CHECK(*it == 'e'); | ||||
|             ++it; | ||||
|             CHECK(it == str.end()); | ||||
|         } | ||||
|         SECTION( "iterates backwards" ) { | ||||
|             auto it = str.end(); | ||||
|             --it; | ||||
|             CHECK(*it == 'e'); | ||||
|             --it; | ||||
|             CHECK(*it == 'd'); | ||||
|             --it; | ||||
|             CHECK(*it == 'c'); | ||||
|             --it; | ||||
|             CHECK(*it == 'b'); | ||||
|             --it; | ||||
|             CHECK(*it == 'a'); | ||||
|             CHECK(it == str.begin()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     SECTION( "ansi escape sequences at the start" ) { | ||||
|         std::string text = "\033[38;2;98;174;239ma\033[38;2;98;174;239mb\033[38mc\033[0md\033[me"; | ||||
|         AnsiSkippingString str(text); | ||||
|         auto it = str.begin(); | ||||
|         CHECK(*it == 'a'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'b'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'c'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'd'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'e'); | ||||
|         ++it; | ||||
|         CHECK(it == str.end()); | ||||
|         --it; | ||||
|         CHECK(*it == 'e'); | ||||
|         --it; | ||||
|         CHECK(*it == 'd'); | ||||
|         --it; | ||||
|         CHECK(*it == 'c'); | ||||
|         --it; | ||||
|         CHECK(*it == 'b'); | ||||
|         --it; | ||||
|         CHECK(*it == 'a'); | ||||
|         CHECK(it == str.begin()); | ||||
|     } | ||||
|  | ||||
|     SECTION( "ansi escape sequences at the end" ) { | ||||
|         std::string text = "a\033[38;2;98;174;239mb\033[38mc\033[0md\033[me\033[38;2;98;174;239m"; | ||||
|         AnsiSkippingString str(text); | ||||
|         auto it = str.begin(); | ||||
|         CHECK(*it == 'a'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'b'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'c'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'd'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'e'); | ||||
|         ++it; | ||||
|         CHECK(it == str.end()); | ||||
|         --it; | ||||
|         CHECK(*it == 'e'); | ||||
|         --it; | ||||
|         CHECK(*it == 'd'); | ||||
|         --it; | ||||
|         CHECK(*it == 'c'); | ||||
|         --it; | ||||
|         CHECK(*it == 'b'); | ||||
|         --it; | ||||
|         CHECK(*it == 'a'); | ||||
|         CHECK(it == str.begin()); | ||||
|     } | ||||
|  | ||||
|     SECTION( "skips consecutive escapes" ) { | ||||
|         std::string text = "\033[38;2;98;174;239m\033[38;2;98;174;239ma\033[38;2;98;174;239mb\033[38m\033[38m\033[38mc\033[0md\033[me"; | ||||
|         AnsiSkippingString str(text); | ||||
|         auto it = str.begin(); | ||||
|         CHECK(*it == 'a'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'b'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'c'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'd'); | ||||
|         ++it; | ||||
|         CHECK(*it == 'e'); | ||||
|         ++it; | ||||
|         CHECK(it == str.end()); | ||||
|         --it; | ||||
|         CHECK(*it == 'e'); | ||||
|         --it; | ||||
|         CHECK(*it == 'd'); | ||||
|         --it; | ||||
|         CHECK(*it == 'c'); | ||||
|         --it; | ||||
|         CHECK(*it == 'b'); | ||||
|         --it; | ||||
|         CHECK(*it == 'a'); | ||||
|         CHECK(it == str.begin()); | ||||
|     } | ||||
|  | ||||
|     SECTION( "handles incomplete ansi sequences" ) { | ||||
|         std::string text = "a\033[b\033[30c\033[30;d\033[30;2e"; | ||||
|         AnsiSkippingString str(text); | ||||
|         CHECK(std::string(str.begin(), str.end()) == text); | ||||
|     } | ||||
| } | ||||
|  | ||||
| TEST_CASE( "TextFlow::AnsiSkippingString computes the size properly", | ||||
|            "[TextFlow][ansiskippingstring][approvals]" ) { | ||||
|     std::string text = "\033[38;2;98;174;239m\033[38;2;98;174;239ma\033[38;2;98;174;239mb\033[38m\033[38m\033[38mc\033[0md\033[me"; | ||||
|     AnsiSkippingString str(text); | ||||
|     CHECK(str.size() == 5); | ||||
| } | ||||
|  | ||||
| TEST_CASE( "TextFlow::AnsiSkippingString substrings properly", | ||||
|            "[TextFlow][ansiskippingstring][approvals]" ) { | ||||
|     SECTION("basic test") { | ||||
|         std::string text = "a\033[38;2;98;174;239mb\033[38mc\033[0md\033[me"; | ||||
|         AnsiSkippingString str(text); | ||||
|         auto a = str.begin(); | ||||
|         auto b = str.begin(); | ||||
|         ++b; | ||||
|         ++b; | ||||
|         CHECK(str.substring(a, b) == "a\033[38;2;98;174;239mb\033[38m"); | ||||
|         ++a; | ||||
|         ++b; | ||||
|         CHECK(str.substring(a, b) == "b\033[38mc\033[0m"); | ||||
|         CHECK(str.substring(a, str.end()) == "b\033[38mc\033[0md\033[me"); | ||||
|         CHECK(str.substring(str.begin(), str.end()) == text); | ||||
|     } | ||||
|     SECTION("escapes at the start") { | ||||
|         std::string text = "\033[38;2;98;174;239m\033[38;2;98;174;239ma\033[38;2;98;174;239mb\033[38m\033[38m\033[38mc\033[0md\033[me"; | ||||
|         AnsiSkippingString str(text); | ||||
|         auto a = str.begin(); | ||||
|         auto b = str.begin(); | ||||
|         ++b; | ||||
|         ++b; | ||||
|         CHECK(str.substring(a, b) == "\033[38;2;98;174;239m\033[38;2;98;174;239ma\033[38;2;98;174;239mb\033[38m\033[38m\033[38m"); | ||||
|         ++a; | ||||
|         ++b; | ||||
|         CHECK(str.substring(a, b) == "b\033[38m\033[38m\033[38mc\033[0m"); | ||||
|         CHECK(str.substring(a, str.end()) == "b\033[38m\033[38m\033[38mc\033[0md\033[me"); | ||||
|         CHECK(str.substring(str.begin(), str.end()) == text); | ||||
|     } | ||||
|     SECTION("escapes at the end") { | ||||
|         std::string text = "a\033[38;2;98;174;239mb\033[38mc\033[0md\033[me\033[38m"; | ||||
|         AnsiSkippingString str(text); | ||||
|         auto a = str.begin(); | ||||
|         auto b = str.begin(); | ||||
|         ++b; | ||||
|         ++b; | ||||
|         CHECK(str.substring(a, b) == "a\033[38;2;98;174;239mb\033[38m"); | ||||
|         ++a; | ||||
|         ++b; | ||||
|         CHECK(str.substring(a, b) == "b\033[38mc\033[0m"); | ||||
|         CHECK(str.substring(a, str.end()) == "b\033[38mc\033[0md\033[me\033[38m"); | ||||
|         CHECK(str.substring(str.begin(), str.end()) == text); | ||||
|     } | ||||
| } | ||||
|  | ||||
| TEST_CASE( "TextFlow::Column skips ansi escape sequences", | ||||
|            "[TextFlow][column][approvals]" ) { | ||||
|     std::string text = "\033[38;2;98;174;239m\033[38;2;198;120;221mThe quick brown \033[38;2;198;120;221mfox jumped over the lazy dog\033[0m"; | ||||
|     Column col(text); | ||||
|  | ||||
|     SECTION( "width=20" ) { | ||||
|         col.width( 20 ); | ||||
|         REQUIRE( as_written( col ) == "\033[38;2;98;174;239m\033[38;2;198;120;221mThe quick brown \033[38;2;198;120;221mfox\n" | ||||
|                                       "jumped over the lazy\n" | ||||
|                                       "dog\033[0m" ); | ||||
|     } | ||||
|  | ||||
|     SECTION( "width=80" ) { | ||||
|         col.width( 80 ); | ||||
|         REQUIRE( as_written( col ) == text ); | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jeremy Rifkin
					Jeremy Rifkin