/* Implementation of text_art::styled_string. Copyright (C) 2023-2025 Free Software Foundation, Inc. Contributed by David Malcolm <dmalcolm@redhat.com>. This file is part of GCC. GCC is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GCC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GCC; see the file COPYING3. If not see <http://www.gnu.org/licenses/>. */ #include "config.h" #define INCLUDE_VECTOR #include "system.h" #include "coretypes.h" #include "make-unique.h" #include "pretty-print.h" #include "intl.h" #include "diagnostic.h" #include "selftest.h" #include "text-art/selftests.h" #include "text-art/types.h" #include "color-macros.h" using namespace text_art; namespace { /* Support class for parsing text containing escape codes. See e.g. https://en.wikipedia.org/wiki/ANSI_escape_code We only support the codes that pretty-print.cc can generate. */ class escape_code_parser { public: escape_code_parser (style_manager &sm, std::vector<styled_unichar> &out) : m_sm (sm), m_out (out), m_cur_style_obj (), m_cur_style_id (style::id_plain), m_state (state::START) { } void on_char (cppchar_t ch) { switch (m_state) { default: gcc_unreachable (); case state::START: if (ch == '\033') { /* The start of an escape sequence. */ m_state = state::AFTER_ESC; return; } break; case state::AFTER_ESC: if (ch == '[') { /* ESC [ is a Control Sequence Introducer. */ m_state = state::CS_PARAMETER_BYTES; return; } else if (ch == ']') { /* ESC ] is an Operating System Command. */ m_state = state::WITHIN_OSC; return; } break; case state::CS_PARAMETER_BYTES: if (parameter_byte_p (ch)) { m_parameter_bytes.push_back ((char)ch); return; } else if (intermediate_byte_p (ch)) { m_intermediate_bytes.push_back ((char)ch); m_state = state::CS_INTERMEDIATE_BYTES; return; } else if (final_byte_p (ch)) { on_final_csi_char (ch); return; } break; case state::CS_INTERMEDIATE_BYTES: /* Expect zero or more intermediate bytes. */ if (intermediate_byte_p (ch)) { m_intermediate_bytes.push_back ((char)ch); return; } else if (final_byte_p (ch)) { on_final_csi_char (ch); return; } break; case state::WITHIN_OSC: /* Accumulate chars into m_osc_string, until we see an ST or a BEL. */ { /* Check for ESC \, the String Terminator (aka "ST"). */ if (ch == '\\' && m_osc_string.size () > 0 && m_osc_string.back () == '\033') { m_osc_string.pop_back (); on_final_osc_char (); return; } else if (ch == '\a') { // BEL on_final_osc_char (); return; } m_osc_string.push_back (ch); return; } break; } /* Test of handling U+FE0F VARIATION SELECTOR-16 to select the emoji variation for the previous character. */ if (ch == 0xFE0F) { if (m_out.size () > 0) m_out.back ().set_emoji_variant (); return; } if (cpp_is_combining_char (ch)) { if (m_out.size () > 0) { m_out.back ().add_combining_char (ch); return; } } /* By default, add the char. */ m_out.push_back (styled_unichar (ch, false, m_cur_style_id)); } private: void on_final_csi_char (cppchar_t ch) { switch (ch) { default: /* Unrecognized. */ break; case 'm': { /* SGR control sequence. */ if (m_parameter_bytes.empty ()) reset_style (); std::vector<int> params (params_from_decimal ()); for (auto iter = params.begin (); iter != params.end (); ) { const int param = *iter; switch (param) { default: /* Unrecognized SGR parameter. */ break; case 0: reset_style (); break; case 1: set_style_bold (); break; case 4: set_style_underscore (); break; case 5: set_style_blink (); break; /* Named foreground colors. */ case 30: set_style_fg_color (style::named_color::BLACK); break; case 31: set_style_fg_color (style::named_color::RED); break; case 32: set_style_fg_color (style::named_color::GREEN); break; case 33: set_style_fg_color (style::named_color::YELLOW); break; case 34: set_style_fg_color (style::named_color::BLUE); break; case 35: set_style_fg_color (style::named_color::MAGENTA); break; case 36: set_style_fg_color (style::named_color::CYAN); break; case 37: set_style_fg_color (style::named_color::WHITE); break; /* 8-bit and 24-bit color */ case 38: case 48: { const bool fg = (param == 38); iter++; if (iter != params.end ()) switch (*(iter++)) { default: break; case 5: /* 8-bit color. */ if (iter != params.end ()) { const uint8_t col = *(iter++); if (fg) set_style_fg_color (style::color (col)); else set_style_bg_color (style::color (col)); } continue; case 2: /* 24-bit color. */ if (iter != params.end ()) { const uint8_t r = *(iter++); if (iter != params.end ()) { const uint8_t g = *(iter++); if (iter != params.end ()) { const uint8_t b = *(iter++); if (fg) set_style_fg_color (style::color (r, g, b)); else set_style_bg_color (style::color (r, g, b)); } } } continue; } continue; } break; /* Named background colors. */ case 40: set_style_bg_color (style::named_color::BLACK); break; case 41: set_style_bg_color (style::named_color::RED); break; case 42: set_style_bg_color (style::named_color::GREEN); break; case 43: set_style_bg_color (style::named_color::YELLOW); break; case 44: set_style_bg_color (style::named_color::BLUE); break; case 45: set_style_bg_color (style::named_color::MAGENTA); break; case 46: set_style_bg_color (style::named_color::CYAN); break; case 47: set_style_bg_color (style::named_color::WHITE); break; /* Named foreground colors, bright. */ case 90: set_style_fg_color (style::color (style::named_color::BLACK, true)); break; case 91: set_style_fg_color (style::color (style::named_color::RED, true)); break; case 92: set_style_fg_color (style::color (style::named_color::GREEN, true)); break; case 93: set_style_fg_color (style::color (style::named_color::YELLOW, true)); break; case 94: set_style_fg_color (style::color (style::named_color::BLUE, true)); break; case 95: set_style_fg_color (style::color (style::named_color::MAGENTA, true)); break; case 96: set_style_fg_color (style::color (style::named_color::CYAN, true)); break; case 97: set_style_fg_color (style::color (style::named_color::WHITE, true)); break; /* Named foreground colors, bright. */ case 100: set_style_bg_color (style::color (style::named_color::BLACK, true)); break; case 101: set_style_bg_color (style::color (style::named_color::RED, true)); break; case 102: set_style_bg_color (style::color (style::named_color::GREEN, true)); break; case 103: set_style_bg_color (style::color (style::named_color::YELLOW, true)); break; case 104: set_style_bg_color (style::color (style::named_color::BLUE, true)); break; case 105: set_style_bg_color (style::color (style::named_color::MAGENTA, true)); break; case 106: set_style_bg_color (style::color (style::named_color::CYAN, true)); break; case 107: set_style_bg_color (style::color (style::named_color::WHITE, true)); break; } ++iter; } } break; } m_parameter_bytes.clear (); m_intermediate_bytes.clear (); m_state = state::START; } void on_final_osc_char () { if (!m_osc_string.empty ()) { switch (m_osc_string[0]) { default: break; case '8': /* Hyperlink support; see: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda We don't support params, so we expect either: (a) "8;;URL" to begin a url (see pp_begin_url), or (b) "8;;" to end a URL (see pp_end_url). */ if (m_osc_string.size () >= 3 && m_osc_string[1] == ';' && m_osc_string[2] == ';') { set_style_url (m_osc_string.begin () + 3, m_osc_string.end ()); } break; } } m_osc_string.clear (); m_state = state::START; } std::vector<int> params_from_decimal () const { std::vector<int> result; int curr_int = -1; for (auto param_ch : m_parameter_bytes) { if (param_ch >= '0' && param_ch <= '9') { if (curr_int == -1) curr_int = 0; else curr_int *= 10; curr_int += param_ch - '0'; } else { if (curr_int != -1) { result.push_back (curr_int); curr_int = -1; } } } if (curr_int != -1) result.push_back (curr_int); return result; } void refresh_style_id () { m_cur_style_id = m_sm.get_or_create_id (m_cur_style_obj); } void reset_style () { m_cur_style_obj = style (); refresh_style_id (); } void set_style_bold () { m_cur_style_obj.m_bold = true; refresh_style_id (); } void set_style_underscore () { m_cur_style_obj.m_underscore = true; refresh_style_id (); } void set_style_blink () { m_cur_style_obj.m_blink = true; refresh_style_id (); } void set_style_fg_color (style::color color) { m_cur_style_obj.m_fg_color = color; refresh_style_id (); } void set_style_bg_color (style::color color) { m_cur_style_obj.m_bg_color = color; refresh_style_id (); } void set_style_url (std::vector<cppchar_t>::iterator begin, std::vector<cppchar_t>::iterator end) { // The empty string means "no URL" m_cur_style_obj.m_url = std::vector<cppchar_t> (begin, end); refresh_style_id (); } static bool parameter_byte_p (cppchar_t ch) { return ch >= 0x30 && ch <= 0x3F; } static bool intermediate_byte_p (cppchar_t ch) { return ch >= 0x20 && ch <= 0x2F; } static bool final_byte_p (cppchar_t ch) { return ch >= 0x40 && ch <= 0x7E; } style_manager &m_sm; std::vector<styled_unichar> &m_out; style m_cur_style_obj; style::id_t m_cur_style_id; /* Handling of control sequences. */ enum class state { START, /* After ESC, expecting '['. */ AFTER_ESC, /* Expecting zero or more parameter bytes, an intermediate byte, or a final byte. */ CS_PARAMETER_BYTES, /* Expecting zero or more intermediate bytes, or a final byte. */ CS_INTERMEDIATE_BYTES, /* Within OSC. */ WITHIN_OSC } m_state; std::vector<char> m_parameter_bytes; std::vector<char> m_intermediate_bytes; std::vector<cppchar_t> m_osc_string; }; } // anon namespace /* class text_art::styled_string. */ /* Construct a styled_string from STR. STR is assumed to be UTF-8 encoded and 0-terminated. Parse SGR formatting chars from being in-band (within in the sequence of chars) to being out-of-band, as style elements. We only support parsing the subset of SGR chars that can be emitted by pretty-print.cc */ styled_string::styled_string (style_manager &sm, const char *str) : m_chars () { escape_code_parser parser (sm, m_chars); /* We don't actually want the display widths here, but it's an easy way to decode UTF-8. */ cpp_char_column_policy policy (8, cpp_wcwidth); cpp_display_width_computation dw (str, strlen (str), policy); while (!dw.done ()) { cpp_decoded_char decoded_char; dw.process_next_codepoint (&decoded_char); if (!decoded_char.m_valid_ch) /* Skip bytes that aren't valid UTF-8. */ continue; /* Decode SGR formatting. */ cppchar_t ch = decoded_char.m_ch; parser.on_char (ch); } } styled_string::styled_string (cppchar_t cppchar, bool emoji) { m_chars.push_back (styled_unichar (cppchar, emoji, style::id_plain)); } styled_string styled_string::from_fmt_va (style_manager &sm, printer_fn format_decoder, const char *fmt, va_list *args) { text_info text (fmt, args, errno); pretty_printer pp; pp_show_color (&pp) = true; pp.set_url_format (URL_FORMAT_DEFAULT); pp_format_decoder (&pp) = format_decoder; pp_format (&pp, &text); pp_output_formatted_text (&pp); styled_string result (sm, pp_formatted_text (&pp)); return result; } styled_string styled_string::from_fmt (style_manager &sm, printer_fn format_decoder, const char *fmt, ...) { va_list ap; va_start (ap, fmt); styled_string result = from_fmt_va (sm, format_decoder, fmt, &ap); va_end (ap); return result; } int styled_string::calc_canvas_width () const { int result = 0; for (auto ch : m_chars) result += ch.get_canvas_width (); return result; } void styled_string::append (const styled_string &suffix) { m_chars.insert<std::vector<styled_unichar>::const_iterator> (m_chars.end (), suffix.begin (), suffix.end ()); } void styled_string::set_url (style_manager &sm, const char *url) { for (auto& ch : m_chars) { const style &existing_style = sm.get_style (ch.get_style_id ()); style with_url (existing_style); with_url.set_style_url (url); ch.m_style_id = sm.get_or_create_id (with_url); } } #if CHECKING_P namespace selftest { static void test_combining_chars () { /* This really ought to be in libcpp, but we don't have selftests there. */ ASSERT_FALSE (cpp_is_combining_char (0)); ASSERT_FALSE (cpp_is_combining_char ('a')); /* COMBINING BREVE (U+0306). */ ASSERT_TRUE (cpp_is_combining_char (0x0306)); /* U+5B57 CJK UNIFIED IDEOGRAPH-5B57. */ ASSERT_FALSE (cpp_is_combining_char (0x5B57)); /* U+FE0F VARIATION SELECTOR-16. */ ASSERT_FALSE (cpp_is_combining_char (0xFE0F)); } static void test_empty () { style_manager sm; styled_string s (sm, ""); ASSERT_EQ (s.size (), 0); ASSERT_EQ (s.calc_canvas_width (), 0); } /* Test of a pure ASCII string with no escape codes. */ static void test_simple () { const char *c_str = "hello world!"; style_manager sm; styled_string s (sm, c_str); ASSERT_EQ (s.size (), strlen (c_str)); ASSERT_EQ (s.calc_canvas_width (), (int)strlen (c_str)); for (size_t i = 0; i < strlen (c_str); i++) { ASSERT_EQ (s[i].get_code (), (cppchar_t)c_str[i]); ASSERT_EQ (s[i].get_style_id (), 0); } } /* Test of decoding UTF-8. */ static void test_pi_from_utf8 () { /* U+03C0 "GREEK SMALL LETTER PI". */ const char * const pi_utf8 = "\xCF\x80"; style_manager sm; styled_string s (sm, pi_utf8); ASSERT_EQ (s.size (), 1); ASSERT_EQ (s.calc_canvas_width (), 1); ASSERT_EQ (s[0].get_code (), 0x03c0); ASSERT_EQ (s[0].emoji_variant_p (), false); ASSERT_EQ (s[0].double_width_p (), false); ASSERT_EQ (s[0].get_style_id (), 0); } /* Test of double-width character. */ static void test_emoji_from_utf8 () { /* U+1F642 "SLIGHTLY SMILING FACE". */ const char * const emoji_utf8 = "\xF0\x9F\x99\x82"; style_manager sm; styled_string s (sm, emoji_utf8); ASSERT_EQ (s.size (), 1); ASSERT_EQ (s.calc_canvas_width (), 2); ASSERT_EQ (s[0].get_code (), 0x1f642); ASSERT_EQ (s[0].double_width_p (), true); ASSERT_EQ (s[0].get_style_id (), 0); } /* Test of handling U+FE0F VARIATION SELECTOR-16 to select the emoji variation for the previous character. */ static void test_emoji_variant_from_utf8 () { const char * const emoji_utf8 = (/* U+26A0 WARNING SIGN. */ "\xE2\x9A\xA0" /* U+FE0F VARIATION SELECTOR-16 (emoji variation selector). */ "\xEF\xB8\x8F"); style_manager sm; styled_string s (sm, emoji_utf8); ASSERT_EQ (s.size (), 1); ASSERT_EQ (s.calc_canvas_width (), 1); ASSERT_EQ (s[0].get_code (), 0x26a0); ASSERT_EQ (s[0].emoji_variant_p (), true); ASSERT_EQ (s[0].double_width_p (), false); ASSERT_EQ (s[0].get_style_id (), 0); } static void test_emoji_from_codepoint () { styled_string s ((cppchar_t)0x1f642); ASSERT_EQ (s.size (), 1); ASSERT_EQ (s.calc_canvas_width (), 2); ASSERT_EQ (s[0].get_code (), 0x1f642); ASSERT_EQ (s[0].double_width_p (), true); ASSERT_EQ (s[0].get_style_id (), 0); } static void test_from_mixed_width_utf8 () { /* This UTF-8 string literal is of the form before mojibake after where the Japanese word "mojibake" is written as the following four unicode code points: U+6587 CJK UNIFIED IDEOGRAPH-6587 U+5B57 CJK UNIFIED IDEOGRAPH-5B57 U+5316 CJK UNIFIED IDEOGRAPH-5316 U+3051 HIRAGANA LETTER KE. Each of these is 3 bytes wide when encoded in UTF-8, whereas the "before" and "after" are 1 byte per unicode character. */ const char * const mixed_width_utf8 = ("before " /* U+6587 CJK UNIFIED IDEOGRAPH-6587 UTF-8: 0xE6 0x96 0x87 C octal escaped UTF-8: \346\226\207. */ "\346\226\207" /* U+5B57 CJK UNIFIED IDEOGRAPH-5B57 UTF-8: 0xE5 0xAD 0x97 C octal escaped UTF-8: \345\255\227. */ "\345\255\227" /* U+5316 CJK UNIFIED IDEOGRAPH-5316 UTF-8: 0xE5 0x8C 0x96 C octal escaped UTF-8: \345\214\226. */ "\345\214\226" /* U+3051 HIRAGANA LETTER KE UTF-8: 0xE3 0x81 0x91 C octal escaped UTF-8: \343\201\221. */ "\343\201\221" " after"); style_manager sm; styled_string s (sm, mixed_width_utf8); ASSERT_EQ (s.size (), 6 + 1 + 4 + 1 + 5); ASSERT_EQ (sm.get_num_styles (), 1); // We expect the Japanese characters to be double width. ASSERT_EQ (s.calc_canvas_width (), 6 + 1 + (2 * 4) + 1 + 5); ASSERT_EQ (s[0].get_code (), 'b'); ASSERT_EQ (s[0].double_width_p (), false); ASSERT_EQ (s[1].get_code (), 'e'); ASSERT_EQ (s[2].get_code (), 'f'); ASSERT_EQ (s[3].get_code (), 'o'); ASSERT_EQ (s[4].get_code (), 'r'); ASSERT_EQ (s[5].get_code (), 'e'); ASSERT_EQ (s[6].get_code (), ' '); ASSERT_EQ (s[7].get_code (), 0x6587); ASSERT_EQ (s[7].double_width_p (), true); ASSERT_EQ (s[8].get_code (), 0x5B57); ASSERT_EQ (s[9].get_code (), 0x5316); ASSERT_EQ (s[10].get_code (), 0x3051); ASSERT_EQ (s[11].get_code (), ' '); ASSERT_EQ (s[12].get_code (), 'a'); ASSERT_EQ (s[13].get_code (), 'f'); ASSERT_EQ (s[14].get_code (), 't'); ASSERT_EQ (s[15].get_code (), 'e'); ASSERT_EQ (s[16].get_code (), 'r'); ASSERT_EQ (s[0].get_style_id (), 0); } static void assert_style_urleq (const location &loc, const style &s, const char *expected_str) { ASSERT_EQ_AT (loc, s.m_url.size (), strlen (expected_str)); for (size_t i = 0; i < s.m_url.size (); i++) ASSERT_EQ_AT (loc, s.m_url[i], (cppchar_t)expected_str[i]); } #define ASSERT_STYLE_URLEQ(STYLE, EXPECTED_STR) \ assert_style_urleq ((SELFTEST_LOCATION), (STYLE), (EXPECTED_STR)) static void test_url () { // URL_FORMAT_ST { style_manager sm; styled_string s (sm, "\33]8;;http://example.com\33\\This is a link\33]8;;\33\\"); const char *expected = "This is a link"; ASSERT_EQ (s.size (), strlen (expected)); ASSERT_EQ (s.calc_canvas_width (), (int)strlen (expected)); ASSERT_EQ (sm.get_num_styles (), 2); for (size_t i = 0; i < strlen (expected); i++) { ASSERT_EQ (s[i].get_code (), (cppchar_t)expected[i]); ASSERT_EQ (s[i].get_style_id (), 1); } ASSERT_STYLE_URLEQ (sm.get_style (1), "http://example.com"); } // URL_FORMAT_BEL { style_manager sm; styled_string s (sm, "\33]8;;http://example.com\aThis is a link\33]8;;\a"); const char *expected = "This is a link"; ASSERT_EQ (s.size (), strlen (expected)); ASSERT_EQ (s.calc_canvas_width (), (int)strlen (expected)); ASSERT_EQ (sm.get_num_styles (), 2); for (size_t i = 0; i < strlen (expected); i++) { ASSERT_EQ (s[i].get_code (), (cppchar_t)expected[i]); ASSERT_EQ (s[i].get_style_id (), 1); } ASSERT_STYLE_URLEQ (sm.get_style (1), "http://example.com"); } } static void test_from_fmt () { style_manager sm; styled_string s (styled_string::from_fmt (sm, NULL, "%%i: %i", 42)); ASSERT_EQ (s[0].get_code (), '%'); ASSERT_EQ (s[1].get_code (), 'i'); ASSERT_EQ (s[2].get_code (), ':'); ASSERT_EQ (s[3].get_code (), ' '); ASSERT_EQ (s[4].get_code (), '4'); ASSERT_EQ (s[5].get_code (), '2'); ASSERT_EQ (s.size (), 6); ASSERT_EQ (s.calc_canvas_width (), 6); } static void test_from_fmt_qs () { auto_fix_quotes fix_quotes; open_quote = "\xe2\x80\x98"; close_quote = "\xe2\x80\x99"; style_manager sm; styled_string s (styled_string::from_fmt (sm, NULL, "%qs", "msg")); ASSERT_EQ (sm.get_num_styles (), 2); ASSERT_EQ (s[0].get_code (), 0x2018); ASSERT_EQ (s[0].get_style_id (), 0); ASSERT_EQ (s[1].get_code (), 'm'); ASSERT_EQ (s[1].get_style_id (), 1); ASSERT_EQ (s[2].get_code (), 's'); ASSERT_EQ (s[2].get_style_id (), 1); ASSERT_EQ (s[3].get_code (), 'g'); ASSERT_EQ (s[3].get_style_id (), 1); ASSERT_EQ (s[4].get_code (), 0x2019); ASSERT_EQ (s[4].get_style_id (), 0); ASSERT_EQ (s.size (), 5); } // Test of parsing SGR codes. static void test_from_str_with_bold () { style_manager sm; /* This is the result of pp_printf (pp, "%qs", "foo") with auto_fix_quotes. */ styled_string s (sm, "`\33[01m\33[Kfoo\33[m\33[K'"); ASSERT_EQ (s[0].get_code (), '`'); ASSERT_EQ (s[0].get_style_id (), 0); ASSERT_EQ (s[1].get_code (), 'f'); ASSERT_EQ (s[1].get_style_id (), 1); ASSERT_EQ (s[2].get_code (), 'o'); ASSERT_EQ (s[2].get_style_id (), 1); ASSERT_EQ (s[3].get_code (), 'o'); ASSERT_EQ (s[3].get_style_id (), 1); ASSERT_EQ (s[4].get_code (), '\''); ASSERT_EQ (s[4].get_style_id (), 0); ASSERT_EQ (s.size (), 5); ASSERT_TRUE (sm.get_style (1).m_bold); } static void test_from_str_with_underscore () { style_manager sm; styled_string s (sm, "\33[04m\33[KA"); ASSERT_EQ (s[0].get_code (), 'A'); ASSERT_EQ (s[0].get_style_id (), 1); ASSERT_TRUE (sm.get_style (1).m_underscore); } static void test_from_str_with_blink () { style_manager sm; styled_string s (sm, "\33[05m\33[KA"); ASSERT_EQ (s[0].get_code (), 'A'); ASSERT_EQ (s[0].get_style_id (), 1); ASSERT_TRUE (sm.get_style (1).m_blink); } // Test of parsing SGR codes. static void test_from_str_with_color () { style_manager sm; styled_string s (sm, ("0" SGR_SEQ (COLOR_FG_RED) "R" SGR_RESET "2" SGR_SEQ (COLOR_FG_GREEN) "G" SGR_RESET "4")); ASSERT_EQ (s.size (), 5); ASSERT_EQ (sm.get_num_styles (), 3); ASSERT_EQ (s[0].get_code (), '0'); ASSERT_EQ (s[0].get_style_id (), 0); ASSERT_EQ (s[1].get_code (), 'R'); ASSERT_EQ (s[1].get_style_id (), 1); ASSERT_EQ (s[2].get_code (), '2'); ASSERT_EQ (s[2].get_style_id (), 0); ASSERT_EQ (s[3].get_code (), 'G'); ASSERT_EQ (s[3].get_style_id (), 2); ASSERT_EQ (s[4].get_code (), '4'); ASSERT_EQ (s[4].get_style_id (), 0); ASSERT_EQ (sm.get_style (1).m_fg_color, style::named_color::RED); ASSERT_EQ (sm.get_style (2).m_fg_color, style::named_color::GREEN); } static void test_from_str_with_named_color () { style_manager sm; styled_string s (sm, ("F" SGR_SEQ (COLOR_FG_BLACK) "F" SGR_SEQ (COLOR_FG_RED) "F" SGR_SEQ (COLOR_FG_GREEN) "F" SGR_SEQ (COLOR_FG_YELLOW) "F" SGR_SEQ (COLOR_FG_BLUE) "F" SGR_SEQ (COLOR_FG_MAGENTA) "F" SGR_SEQ (COLOR_FG_CYAN) "F" SGR_SEQ (COLOR_FG_WHITE) "F" SGR_SEQ (COLOR_FG_BRIGHT_BLACK) "F" SGR_SEQ (COLOR_FG_BRIGHT_RED) "F" SGR_SEQ (COLOR_FG_BRIGHT_GREEN) "F" SGR_SEQ (COLOR_FG_BRIGHT_YELLOW) "F" SGR_SEQ (COLOR_FG_BRIGHT_BLUE) "F" SGR_SEQ (COLOR_FG_BRIGHT_MAGENTA) "F" SGR_SEQ (COLOR_FG_BRIGHT_CYAN) "F" SGR_SEQ (COLOR_FG_BRIGHT_WHITE) "F" SGR_SEQ (COLOR_BG_BLACK) "B" SGR_SEQ (COLOR_BG_RED) "B" SGR_SEQ (COLOR_BG_GREEN) "B" SGR_SEQ (COLOR_BG_YELLOW) "B" SGR_SEQ (COLOR_BG_BLUE) "B" SGR_SEQ (COLOR_BG_MAGENTA) "B" SGR_SEQ (COLOR_BG_CYAN) "B" SGR_SEQ (COLOR_BG_WHITE) "B" SGR_SEQ (COLOR_BG_BRIGHT_BLACK) "B" SGR_SEQ (COLOR_BG_BRIGHT_RED) "B" SGR_SEQ (COLOR_BG_BRIGHT_GREEN) "B" SGR_SEQ (COLOR_BG_BRIGHT_YELLOW) "B" SGR_SEQ (COLOR_BG_BRIGHT_BLUE) "B" SGR_SEQ (COLOR_BG_BRIGHT_MAGENTA) "B" SGR_SEQ (COLOR_BG_BRIGHT_CYAN) "B" SGR_SEQ (COLOR_BG_BRIGHT_WHITE) "B")); ASSERT_EQ (s.size (), 33); for (size_t i = 0; i < s.size (); i++) ASSERT_EQ (s[i].get_style_id (), i); for (size_t i = 0; i < 17; i++) ASSERT_EQ (s[i].get_code (), 'F'); for (size_t i = 17; i < 33; i++) ASSERT_EQ (s[i].get_code (), 'B'); } static void test_from_str_with_8_bit_color () { { style_manager sm; styled_string s (sm, ("[38;5;232m[KF")); ASSERT_EQ (s.size (), 1); ASSERT_EQ (s[0].get_code (), 'F'); ASSERT_EQ (s[0].get_style_id (), 1); ASSERT_EQ (sm.get_style (1).m_fg_color, style::color (232)); } { style_manager sm; styled_string s (sm, ("[48;5;231m[KB")); ASSERT_EQ (s.size (), 1); ASSERT_EQ (s[0].get_code (), 'B'); ASSERT_EQ (s[0].get_style_id (), 1); ASSERT_EQ (sm.get_style (1).m_bg_color, style::color (231)); } } static void test_from_str_with_24_bit_color () { { style_manager sm; styled_string s (sm, ("[38;2;243;250;242m[KF")); ASSERT_EQ (s.size (), 1); ASSERT_EQ (s[0].get_code (), 'F'); ASSERT_EQ (s[0].get_style_id (), 1); ASSERT_EQ (sm.get_style (1).m_fg_color, style::color (243, 250, 242)); } { style_manager sm; styled_string s (sm, ("[48;2;253;247;231m[KB")); ASSERT_EQ (s.size (), 1); ASSERT_EQ (s[0].get_code (), 'B'); ASSERT_EQ (s[0].get_style_id (), 1); ASSERT_EQ (sm.get_style (1).m_bg_color, style::color (253, 247, 231)); } } static void test_from_str_combining_characters () { style_manager sm; styled_string s (sm, /* CYRILLIC CAPITAL LETTER U (U+0423). */ "\xD0\xA3" /* COMBINING BREVE (U+0306). */ "\xCC\x86"); ASSERT_EQ (s.size (), 1); ASSERT_EQ (s[0].get_code (), 0x423); ASSERT_EQ (s[0].get_combining_chars ().size (), 1); ASSERT_EQ (s[0].get_combining_chars ()[0], 0x306); } /* Run all selftests in this file. */ void text_art_styled_string_cc_tests () { test_combining_chars (); test_empty (); test_simple (); test_pi_from_utf8 (); test_emoji_from_utf8 (); test_emoji_variant_from_utf8 (); test_emoji_from_codepoint (); test_from_mixed_width_utf8 (); test_url (); test_from_fmt (); test_from_fmt_qs (); test_from_str_with_bold (); test_from_str_with_underscore (); test_from_str_with_blink (); test_from_str_with_color (); test_from_str_with_named_color (); test_from_str_with_8_bit_color (); test_from_str_with_24_bit_color (); test_from_str_combining_characters (); } } // namespace selftest #endif /* #if CHECKING_P */