blob: 3b7145f9efb0986e3896ff10c9970bfad0a66254 [file] [log] [blame]
/* 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 "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,
("F"));
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,
("B"));
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,
("F"));
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,
("B"));
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 */