/* 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,
		     ("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 */