mirror of
https://forge.sourceware.org/marek/gcc.git
synced 2026-02-22 03:47:02 -05:00
1104 lines
27 KiB
C++
1104 lines
27 KiB
C++
/* Implementation of text_art::styled_string.
|
||
Copyright (C) 2023-2026 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,
|
||
("[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 */
|