Line data Source code
1 : /* Implementation of text_art::styled_string.
2 : Copyright (C) 2023-2026 Free Software Foundation, Inc.
3 : Contributed by David Malcolm <dmalcolm@redhat.com>.
4 :
5 : This file is part of GCC.
6 :
7 : GCC is free software; you can redistribute it and/or modify it under
8 : the terms of the GNU General Public License as published by the Free
9 : Software Foundation; either version 3, or (at your option) any later
10 : version.
11 :
12 : GCC is distributed in the hope that it will be useful, but WITHOUT ANY
13 : WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 : FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 : for more details.
16 :
17 : You should have received a copy of the GNU General Public License
18 : along with GCC; see the file COPYING3. If not see
19 : <http://www.gnu.org/licenses/>. */
20 :
21 : #include "config.h"
22 : #define INCLUDE_VECTOR
23 : #include "system.h"
24 : #include "coretypes.h"
25 : #include "pretty-print.h"
26 : #include "intl.h"
27 : #include "diagnostic.h"
28 : #include "selftest.h"
29 : #include "text-art/selftests.h"
30 : #include "text-art/types.h"
31 : #include "color-macros.h"
32 :
33 : using namespace text_art;
34 :
35 : namespace {
36 :
37 : /* Support class for parsing text containing escape codes.
38 : See e.g. https://en.wikipedia.org/wiki/ANSI_escape_code
39 : We only support the codes that pretty-print.cc can generate. */
40 :
41 : class escape_code_parser
42 : {
43 : public:
44 11380 : escape_code_parser (style_manager &sm,
45 : std::vector<styled_unichar> &out)
46 11380 : : m_sm (sm),
47 11380 : m_out (out),
48 11380 : m_cur_style_obj (),
49 11380 : m_cur_style_id (style::id_plain),
50 11380 : m_state (state::START)
51 : {
52 11380 : }
53 :
54 177807 : void on_char (cppchar_t ch)
55 : {
56 177807 : switch (m_state)
57 : {
58 0 : default:
59 0 : gcc_unreachable ();
60 170771 : case state::START:
61 170771 : if (ch == '\033')
62 : {
63 : /* The start of an escape sequence. */
64 2320 : m_state = state::AFTER_ESC;
65 2320 : return;
66 : }
67 : break;
68 2320 : case state::AFTER_ESC:
69 2320 : if (ch == '[')
70 : {
71 : /* ESC [ is a Control Sequence Introducer. */
72 2304 : m_state = state::CS_PARAMETER_BYTES;
73 2304 : return;
74 : }
75 16 : else if (ch == ']')
76 : {
77 : /* ESC ] is an Operating System Command. */
78 16 : m_state = state::WITHIN_OSC;
79 16 : return;
80 : }
81 : break;
82 4500 : case state::CS_PARAMETER_BYTES:
83 4500 : if (parameter_byte_p (ch))
84 : {
85 2196 : m_parameter_bytes.push_back ((char)ch);
86 2196 : return;
87 : }
88 2304 : else if (intermediate_byte_p (ch))
89 : {
90 0 : m_intermediate_bytes.push_back ((char)ch);
91 0 : m_state = state::CS_INTERMEDIATE_BYTES;
92 0 : return;
93 : }
94 2304 : else if (final_byte_p (ch))
95 : {
96 2304 : on_final_csi_char (ch);
97 2304 : return;
98 : }
99 : break;
100 0 : case state::CS_INTERMEDIATE_BYTES:
101 : /* Expect zero or more intermediate bytes. */
102 0 : if (intermediate_byte_p (ch))
103 : {
104 0 : m_intermediate_bytes.push_back ((char)ch);
105 0 : return;
106 : }
107 0 : else if (final_byte_p (ch))
108 : {
109 0 : on_final_csi_char (ch);
110 0 : return;
111 : }
112 : break;
113 216 : case state::WITHIN_OSC:
114 : /* Accumulate chars into m_osc_string, until we see an ST or a BEL. */
115 216 : {
116 : /* Check for ESC \, the String Terminator (aka "ST"). */
117 216 : if (ch == '\\'
118 8 : && m_osc_string.size () > 0
119 224 : && m_osc_string.back () == '\033')
120 : {
121 8 : m_osc_string.pop_back ();
122 8 : on_final_osc_char ();
123 8 : return;
124 : }
125 208 : else if (ch == '\a')
126 : {
127 : // BEL
128 8 : on_final_osc_char ();
129 8 : return;
130 : }
131 200 : m_osc_string.push_back (ch);
132 200 : return;
133 : }
134 168451 : break;
135 : }
136 :
137 : /* Test of handling U+FE0F VARIATION SELECTOR-16 to select the emoji
138 : variation for the previous character. */
139 168451 : if (ch == 0xFE0F)
140 : {
141 6 : if (m_out.size () > 0)
142 6 : m_out.back ().set_emoji_variant ();
143 6 : return;
144 : }
145 :
146 168445 : if (cpp_is_combining_char (ch))
147 : {
148 4 : if (m_out.size () > 0)
149 : {
150 4 : m_out.back ().add_combining_char (ch);
151 4 : return;
152 : }
153 : }
154 : /* By default, add the char. */
155 168441 : m_out.push_back (styled_unichar (ch, false, m_cur_style_id));
156 : }
157 :
158 : private:
159 2304 : void on_final_csi_char (cppchar_t ch)
160 : {
161 2304 : switch (ch)
162 : {
163 : default:
164 : /* Unrecognized. */
165 : break;
166 1152 : case 'm':
167 1152 : {
168 : /* SGR control sequence. */
169 1152 : if (m_parameter_bytes.empty ())
170 426 : reset_style ();
171 1152 : std::vector<int> params (params_from_decimal ());
172 2062 : for (auto iter = params.begin (); iter != params.end (); )
173 : {
174 910 : const int param = *iter;
175 910 : switch (param)
176 : {
177 : default:
178 : /* Unrecognized SGR parameter. */
179 : break;
180 0 : case 0:
181 0 : reset_style ();
182 0 : break;
183 566 : case 1:
184 566 : set_style_bold ();
185 566 : break;
186 4 : case 4:
187 4 : set_style_underscore ();
188 4 : break;
189 4 : case 5:
190 4 : set_style_blink ();
191 4 : break;
192 :
193 : /* Named foreground colors. */
194 4 : case 30:
195 4 : set_style_fg_color (style::named_color::BLACK);
196 4 : break;
197 80 : case 31:
198 80 : set_style_fg_color (style::named_color::RED);
199 80 : break;
200 80 : case 32:
201 80 : set_style_fg_color (style::named_color::GREEN);
202 80 : break;
203 4 : case 33:
204 4 : set_style_fg_color (style::named_color::YELLOW);
205 4 : break;
206 4 : case 34:
207 4 : set_style_fg_color (style::named_color::BLUE);
208 4 : break;
209 4 : case 35:
210 4 : set_style_fg_color (style::named_color::MAGENTA);
211 4 : break;
212 44 : case 36:
213 44 : set_style_fg_color (style::named_color::CYAN);
214 44 : break;
215 4 : case 37:
216 4 : set_style_fg_color (style::named_color::WHITE);
217 4 : break;
218 :
219 : /* 8-bit and 24-bit color */
220 16 : case 38:
221 16 : case 48:
222 16 : {
223 16 : const bool fg = (param == 38);
224 16 : iter++;
225 16 : if (iter != params.end ())
226 16 : switch (*(iter++))
227 : {
228 : default:
229 : break;
230 8 : case 5:
231 : /* 8-bit color. */
232 8 : if (iter != params.end ())
233 : {
234 8 : const uint8_t col = *(iter++);
235 8 : if (fg)
236 4 : set_style_fg_color (style::color (col));
237 : else
238 4 : set_style_bg_color (style::color (col));
239 : }
240 8 : continue;
241 8 : case 2:
242 : /* 24-bit color. */
243 8 : if (iter != params.end ())
244 : {
245 8 : const uint8_t r = *(iter++);
246 8 : if (iter != params.end ())
247 : {
248 8 : const uint8_t g = *(iter++);
249 8 : if (iter != params.end ())
250 : {
251 8 : const uint8_t b = *(iter++);
252 8 : if (fg)
253 8 : set_style_fg_color (style::color (r,
254 : g,
255 4 : b));
256 : else
257 8 : set_style_bg_color (style::color (r,
258 : g,
259 4 : b));
260 : }
261 : }
262 : }
263 8 : continue;
264 16 : }
265 0 : continue;
266 0 : }
267 : break;
268 :
269 : /* Named background colors. */
270 4 : case 40:
271 4 : set_style_bg_color (style::named_color::BLACK);
272 4 : break;
273 4 : case 41:
274 4 : set_style_bg_color (style::named_color::RED);
275 4 : break;
276 4 : case 42:
277 4 : set_style_bg_color (style::named_color::GREEN);
278 4 : break;
279 4 : case 43:
280 4 : set_style_bg_color (style::named_color::YELLOW);
281 4 : break;
282 4 : case 44:
283 4 : set_style_bg_color (style::named_color::BLUE);
284 4 : break;
285 4 : case 45:
286 4 : set_style_bg_color (style::named_color::MAGENTA);
287 4 : break;
288 4 : case 46:
289 4 : set_style_bg_color (style::named_color::CYAN);
290 4 : break;
291 4 : case 47:
292 4 : set_style_bg_color (style::named_color::WHITE);
293 4 : break;
294 :
295 : /* Named foreground colors, bright. */
296 4 : case 90:
297 4 : set_style_fg_color (style::color (style::named_color::BLACK,
298 4 : true));
299 4 : break;
300 4 : case 91:
301 4 : set_style_fg_color (style::color (style::named_color::RED,
302 4 : true));
303 4 : break;
304 4 : case 92:
305 4 : set_style_fg_color (style::color (style::named_color::GREEN,
306 4 : true));
307 4 : break;
308 4 : case 93:
309 4 : set_style_fg_color (style::color (style::named_color::YELLOW,
310 4 : true));
311 4 : break;
312 4 : case 94:
313 4 : set_style_fg_color (style::color (style::named_color::BLUE,
314 4 : true));
315 4 : break;
316 4 : case 95:
317 4 : set_style_fg_color (style::color (style::named_color::MAGENTA,
318 4 : true));
319 4 : break;
320 4 : case 96:
321 4 : set_style_fg_color (style::color (style::named_color::CYAN,
322 4 : true));
323 4 : break;
324 4 : case 97:
325 4 : set_style_fg_color (style::color (style::named_color::WHITE,
326 4 : true));
327 4 : break;
328 :
329 : /* Named foreground colors, bright. */
330 4 : case 100:
331 4 : set_style_bg_color (style::color (style::named_color::BLACK,
332 4 : true));
333 4 : break;
334 4 : case 101:
335 4 : set_style_bg_color (style::color (style::named_color::RED,
336 4 : true));
337 4 : break;
338 4 : case 102:
339 4 : set_style_bg_color (style::color (style::named_color::GREEN,
340 4 : true));
341 4 : break;
342 4 : case 103:
343 4 : set_style_bg_color (style::color (style::named_color::YELLOW,
344 4 : true));
345 4 : break;
346 4 : case 104:
347 4 : set_style_bg_color (style::color (style::named_color::BLUE,
348 4 : true));
349 4 : break;
350 4 : case 105:
351 4 : set_style_bg_color (style::color (style::named_color::MAGENTA,
352 4 : true));
353 4 : break;
354 4 : case 106:
355 4 : set_style_bg_color (style::color (style::named_color::CYAN,
356 4 : true));
357 4 : break;
358 4 : case 107:
359 4 : set_style_bg_color (style::color (style::named_color::WHITE,
360 4 : true));
361 4 : break;
362 : }
363 894 : ++iter;
364 : }
365 1152 : }
366 1152 : break;
367 : }
368 2304 : m_parameter_bytes.clear ();
369 2304 : m_intermediate_bytes.clear ();
370 2304 : m_state = state::START;
371 2304 : }
372 :
373 16 : void on_final_osc_char ()
374 : {
375 16 : if (!m_osc_string.empty ())
376 : {
377 16 : switch (m_osc_string[0])
378 : {
379 : default:
380 : break;
381 16 : case '8':
382 : /* Hyperlink support; see:
383 : https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
384 : We don't support params, so we expect either:
385 : (a) "8;;URL" to begin a url (see pp_begin_url), or
386 : (b) "8;;" to end a URL (see pp_end_url). */
387 16 : if (m_osc_string.size () >= 3
388 16 : && m_osc_string[1] == ';'
389 32 : && m_osc_string[2] == ';')
390 : {
391 16 : set_style_url (m_osc_string.begin () + 3,
392 : m_osc_string.end ());
393 : }
394 : break;
395 : }
396 : }
397 16 : m_osc_string.clear ();
398 16 : m_state = state::START;
399 16 : }
400 :
401 1152 : std::vector<int> params_from_decimal () const
402 : {
403 1152 : std::vector<int> result;
404 :
405 1152 : int curr_int = -1;
406 3348 : for (auto param_ch : m_parameter_bytes)
407 : {
408 2196 : if (param_ch >= '0' && param_ch <= '9')
409 : {
410 1964 : if (curr_int == -1)
411 958 : curr_int = 0;
412 : else
413 1006 : curr_int *= 10;
414 1964 : curr_int += param_ch - '0';
415 : }
416 : else
417 : {
418 232 : if (curr_int != -1)
419 : {
420 232 : result.push_back (curr_int);
421 232 : curr_int = -1;
422 : }
423 : }
424 : }
425 1152 : if (curr_int != -1)
426 726 : result.push_back (curr_int);
427 1152 : return result;
428 : }
429 :
430 1352 : void refresh_style_id ()
431 : {
432 2704 : m_cur_style_id = m_sm.get_or_create_id (m_cur_style_obj);
433 : }
434 426 : void reset_style ()
435 : {
436 426 : m_cur_style_obj = style ();
437 426 : refresh_style_id ();
438 426 : }
439 566 : void set_style_bold ()
440 : {
441 566 : m_cur_style_obj.m_bold = true;
442 566 : refresh_style_id ();
443 566 : }
444 4 : void set_style_underscore ()
445 : {
446 4 : m_cur_style_obj.m_underscore = true;
447 4 : refresh_style_id ();
448 4 : }
449 4 : void set_style_blink ()
450 : {
451 4 : m_cur_style_obj.m_blink = true;
452 4 : refresh_style_id ();
453 4 : }
454 264 : void set_style_fg_color (style::color color)
455 : {
456 264 : m_cur_style_obj.m_fg_color = color;
457 264 : refresh_style_id ();
458 : }
459 72 : void set_style_bg_color (style::color color)
460 : {
461 72 : m_cur_style_obj.m_bg_color = color;
462 72 : refresh_style_id ();
463 : }
464 16 : void set_style_url (std::vector<cppchar_t>::iterator begin,
465 : std::vector<cppchar_t>::iterator end)
466 : {
467 : // The empty string means "no URL"
468 16 : m_cur_style_obj.m_url = std::vector<cppchar_t> (begin, end);
469 16 : refresh_style_id ();
470 16 : }
471 :
472 4500 : static bool parameter_byte_p (cppchar_t ch)
473 : {
474 4500 : return ch >= 0x30 && ch <= 0x3F;
475 : }
476 :
477 2304 : static bool intermediate_byte_p (cppchar_t ch)
478 : {
479 2304 : return ch >= 0x20 && ch <= 0x2F;
480 : }
481 :
482 2304 : static bool final_byte_p (cppchar_t ch)
483 : {
484 2304 : return ch >= 0x40 && ch <= 0x7E;
485 : }
486 :
487 : style_manager &m_sm;
488 : std::vector<styled_unichar> &m_out;
489 :
490 : style m_cur_style_obj;
491 : style::id_t m_cur_style_id;
492 :
493 : /* Handling of control sequences. */
494 : enum class state
495 : {
496 : START,
497 :
498 : /* After ESC, expecting '['. */
499 : AFTER_ESC,
500 :
501 : /* Expecting zero or more parameter bytes, an
502 : intermediate byte, or a final byte. */
503 : CS_PARAMETER_BYTES,
504 :
505 : /* Expecting zero or more intermediate bytes, or a final byte. */
506 : CS_INTERMEDIATE_BYTES,
507 :
508 : /* Within OSC. */
509 : WITHIN_OSC
510 :
511 : } m_state;
512 : std::vector<char> m_parameter_bytes;
513 : std::vector<char> m_intermediate_bytes;
514 : std::vector<cppchar_t> m_osc_string;
515 : };
516 :
517 : } // anon namespace
518 :
519 : /* class text_art::styled_string. */
520 :
521 : /* Construct a styled_string from STR.
522 : STR is assumed to be UTF-8 encoded and 0-terminated.
523 :
524 : Parse SGR formatting chars from being in-band (within in the sequence
525 : of chars) to being out-of-band, as style elements.
526 : We only support parsing the subset of SGR chars that can be emitted
527 : by pretty-print.cc */
528 :
529 11380 : styled_string::styled_string (style_manager &sm, const char *str)
530 11380 : : m_chars ()
531 : {
532 11380 : escape_code_parser parser (sm, m_chars);
533 :
534 : /* We don't actually want the display widths here, but
535 : it's an easy way to decode UTF-8. */
536 11380 : cpp_char_column_policy policy (8, cpp_wcwidth);
537 11380 : cpp_display_width_computation dw (str, strlen (str), policy);
538 200567 : while (!dw.done ())
539 : {
540 177807 : cpp_decoded_char decoded_char;
541 177807 : dw.process_next_codepoint (&decoded_char);
542 :
543 177807 : if (!decoded_char.m_valid_ch)
544 : /* Skip bytes that aren't valid UTF-8. */
545 0 : continue;
546 :
547 : /* Decode SGR formatting. */
548 177807 : cppchar_t ch = decoded_char.m_ch;
549 177807 : parser.on_char (ch);
550 : }
551 11380 : }
552 :
553 105 : styled_string::styled_string (cppchar_t cppchar, bool emoji)
554 : {
555 105 : m_chars.push_back (styled_unichar (cppchar, emoji, style::id_plain));
556 105 : }
557 :
558 : styled_string
559 1400 : styled_string::from_fmt_va (style_manager &sm,
560 : printer_fn format_decoder,
561 : const char *fmt,
562 : va_list *args)
563 : {
564 1400 : text_info text (fmt, args, errno);
565 1400 : pretty_printer pp;
566 1400 : pp_show_color (&pp) = true;
567 1400 : pp.set_url_format (URL_FORMAT_DEFAULT);
568 1400 : pp_format_decoder (&pp) = format_decoder;
569 1400 : pp_format (&pp, &text);
570 1400 : pp_output_formatted_text (&pp);
571 1400 : styled_string result (sm, pp_formatted_text (&pp));
572 2800 : return result;
573 1400 : }
574 :
575 : styled_string
576 528 : styled_string::from_fmt (style_manager &sm,
577 : printer_fn format_decoder,
578 : const char *fmt, ...)
579 : {
580 528 : va_list ap;
581 528 : va_start (ap, fmt);
582 528 : styled_string result = from_fmt_va (sm, format_decoder, fmt, &ap);
583 528 : va_end (ap);
584 528 : return result;
585 : }
586 :
587 : int
588 11276 : styled_string::calc_canvas_width () const
589 : {
590 11276 : int result = 0;
591 180851 : for (auto ch : m_chars)
592 169575 : result += ch.get_canvas_width ();
593 11276 : return result;
594 : }
595 :
596 : void
597 23 : styled_string::append (const styled_string &suffix)
598 : {
599 23 : m_chars.insert<std::vector<styled_unichar>::const_iterator> (m_chars.end (),
600 : suffix.begin (),
601 : suffix.end ());
602 23 : }
603 :
604 : void
605 8 : styled_string::set_url (style_manager &sm, const char *url)
606 : {
607 32 : for (auto& ch : m_chars)
608 : {
609 24 : const style &existing_style = sm.get_style (ch.get_style_id ());
610 24 : style with_url (existing_style);
611 24 : with_url.set_style_url (url);
612 24 : ch.m_style_id = sm.get_or_create_id (with_url);
613 24 : }
614 8 : }
615 :
616 : #if CHECKING_P
617 :
618 : namespace selftest {
619 :
620 : static void
621 4 : test_combining_chars ()
622 : {
623 : /* This really ought to be in libcpp, but we don't have
624 : selftests there. */
625 4 : ASSERT_FALSE (cpp_is_combining_char (0));
626 4 : ASSERT_FALSE (cpp_is_combining_char ('a'));
627 :
628 : /* COMBINING BREVE (U+0306). */
629 4 : ASSERT_TRUE (cpp_is_combining_char (0x0306));
630 :
631 : /* U+5B57 CJK UNIFIED IDEOGRAPH-5B57. */
632 4 : ASSERT_FALSE (cpp_is_combining_char (0x5B57));
633 :
634 : /* U+FE0F VARIATION SELECTOR-16. */
635 4 : ASSERT_FALSE (cpp_is_combining_char (0xFE0F));
636 4 : }
637 :
638 : static void
639 4 : test_empty ()
640 : {
641 4 : style_manager sm;
642 4 : styled_string s (sm, "");
643 4 : ASSERT_EQ (s.size (), 0);
644 4 : ASSERT_EQ (s.calc_canvas_width (), 0);
645 4 : }
646 :
647 : /* Test of a pure ASCII string with no escape codes. */
648 :
649 : static void
650 4 : test_simple ()
651 : {
652 4 : const char *c_str = "hello world!";
653 4 : style_manager sm;
654 4 : styled_string s (sm, c_str);
655 4 : ASSERT_EQ (s.size (), strlen (c_str));
656 4 : ASSERT_EQ (s.calc_canvas_width (), (int)strlen (c_str));
657 52 : for (size_t i = 0; i < strlen (c_str); i++)
658 : {
659 48 : ASSERT_EQ (s[i].get_code (), (cppchar_t)c_str[i]);
660 48 : ASSERT_EQ (s[i].get_style_id (), 0);
661 : }
662 4 : }
663 :
664 : /* Test of decoding UTF-8. */
665 :
666 : static void
667 4 : test_pi_from_utf8 ()
668 : {
669 : /* U+03C0 "GREEK SMALL LETTER PI". */
670 4 : const char * const pi_utf8 = "\xCF\x80";
671 :
672 4 : style_manager sm;
673 4 : styled_string s (sm, pi_utf8);
674 4 : ASSERT_EQ (s.size (), 1);
675 4 : ASSERT_EQ (s.calc_canvas_width (), 1);
676 4 : ASSERT_EQ (s[0].get_code (), 0x03c0);
677 4 : ASSERT_EQ (s[0].emoji_variant_p (), false);
678 4 : ASSERT_EQ (s[0].double_width_p (), false);
679 4 : ASSERT_EQ (s[0].get_style_id (), 0);
680 4 : }
681 :
682 : /* Test of double-width character. */
683 :
684 : static void
685 4 : test_emoji_from_utf8 ()
686 : {
687 : /* U+1F642 "SLIGHTLY SMILING FACE". */
688 4 : const char * const emoji_utf8 = "\xF0\x9F\x99\x82";
689 :
690 4 : style_manager sm;
691 4 : styled_string s (sm, emoji_utf8);
692 4 : ASSERT_EQ (s.size (), 1);
693 4 : ASSERT_EQ (s.calc_canvas_width (), 2);
694 4 : ASSERT_EQ (s[0].get_code (), 0x1f642);
695 4 : ASSERT_EQ (s[0].double_width_p (), true);
696 4 : ASSERT_EQ (s[0].get_style_id (), 0);
697 4 : }
698 :
699 : /* Test of handling U+FE0F VARIATION SELECTOR-16 to select the emoji
700 : variation for the previous character. */
701 :
702 : static void
703 4 : test_emoji_variant_from_utf8 ()
704 : {
705 4 : const char * const emoji_utf8
706 : = (/* U+26A0 WARNING SIGN. */
707 : "\xE2\x9A\xA0"
708 : /* U+FE0F VARIATION SELECTOR-16 (emoji variation selector). */
709 : "\xEF\xB8\x8F");
710 :
711 4 : style_manager sm;
712 4 : styled_string s (sm, emoji_utf8);
713 4 : ASSERT_EQ (s.size (), 1);
714 4 : ASSERT_EQ (s.calc_canvas_width (), 1);
715 4 : ASSERT_EQ (s[0].get_code (), 0x26a0);
716 4 : ASSERT_EQ (s[0].emoji_variant_p (), true);
717 4 : ASSERT_EQ (s[0].double_width_p (), false);
718 4 : ASSERT_EQ (s[0].get_style_id (), 0);
719 4 : }
720 :
721 : static void
722 4 : test_emoji_from_codepoint ()
723 : {
724 4 : styled_string s ((cppchar_t)0x1f642);
725 4 : ASSERT_EQ (s.size (), 1);
726 4 : ASSERT_EQ (s.calc_canvas_width (), 2);
727 4 : ASSERT_EQ (s[0].get_code (), 0x1f642);
728 4 : ASSERT_EQ (s[0].double_width_p (), true);
729 4 : ASSERT_EQ (s[0].get_style_id (), 0);
730 4 : }
731 :
732 : static void
733 4 : test_from_mixed_width_utf8 ()
734 : {
735 : /* This UTF-8 string literal is of the form
736 : before mojibake after
737 : where the Japanese word "mojibake" is written as the following
738 : four unicode code points:
739 : U+6587 CJK UNIFIED IDEOGRAPH-6587
740 : U+5B57 CJK UNIFIED IDEOGRAPH-5B57
741 : U+5316 CJK UNIFIED IDEOGRAPH-5316
742 : U+3051 HIRAGANA LETTER KE.
743 : Each of these is 3 bytes wide when encoded in UTF-8, whereas the
744 : "before" and "after" are 1 byte per unicode character. */
745 4 : const char * const mixed_width_utf8
746 : = ("before "
747 :
748 : /* U+6587 CJK UNIFIED IDEOGRAPH-6587
749 : UTF-8: 0xE6 0x96 0x87
750 : C octal escaped UTF-8: \346\226\207. */
751 : "\346\226\207"
752 :
753 : /* U+5B57 CJK UNIFIED IDEOGRAPH-5B57
754 : UTF-8: 0xE5 0xAD 0x97
755 : C octal escaped UTF-8: \345\255\227. */
756 : "\345\255\227"
757 :
758 : /* U+5316 CJK UNIFIED IDEOGRAPH-5316
759 : UTF-8: 0xE5 0x8C 0x96
760 : C octal escaped UTF-8: \345\214\226. */
761 : "\345\214\226"
762 :
763 : /* U+3051 HIRAGANA LETTER KE
764 : UTF-8: 0xE3 0x81 0x91
765 : C octal escaped UTF-8: \343\201\221. */
766 : "\343\201\221"
767 :
768 : " after");
769 :
770 4 : style_manager sm;
771 4 : styled_string s (sm, mixed_width_utf8);
772 4 : ASSERT_EQ (s.size (), 6 + 1 + 4 + 1 + 5);
773 4 : ASSERT_EQ (sm.get_num_styles (), 1);
774 :
775 : // We expect the Japanese characters to be double width.
776 4 : ASSERT_EQ (s.calc_canvas_width (), 6 + 1 + (2 * 4) + 1 + 5);
777 :
778 4 : ASSERT_EQ (s[0].get_code (), 'b');
779 4 : ASSERT_EQ (s[0].double_width_p (), false);
780 4 : ASSERT_EQ (s[1].get_code (), 'e');
781 4 : ASSERT_EQ (s[2].get_code (), 'f');
782 4 : ASSERT_EQ (s[3].get_code (), 'o');
783 4 : ASSERT_EQ (s[4].get_code (), 'r');
784 4 : ASSERT_EQ (s[5].get_code (), 'e');
785 4 : ASSERT_EQ (s[6].get_code (), ' ');
786 4 : ASSERT_EQ (s[7].get_code (), 0x6587);
787 4 : ASSERT_EQ (s[7].double_width_p (), true);
788 4 : ASSERT_EQ (s[8].get_code (), 0x5B57);
789 4 : ASSERT_EQ (s[9].get_code (), 0x5316);
790 4 : ASSERT_EQ (s[10].get_code (), 0x3051);
791 4 : ASSERT_EQ (s[11].get_code (), ' ');
792 4 : ASSERT_EQ (s[12].get_code (), 'a');
793 4 : ASSERT_EQ (s[13].get_code (), 'f');
794 4 : ASSERT_EQ (s[14].get_code (), 't');
795 4 : ASSERT_EQ (s[15].get_code (), 'e');
796 4 : ASSERT_EQ (s[16].get_code (), 'r');
797 :
798 4 : ASSERT_EQ (s[0].get_style_id (), 0);
799 4 : }
800 :
801 : static void
802 8 : assert_style_urleq (const location &loc,
803 : const style &s,
804 : const char *expected_str)
805 : {
806 8 : ASSERT_EQ_AT (loc, s.m_url.size (), strlen (expected_str));
807 152 : for (size_t i = 0; i < s.m_url.size (); i++)
808 144 : ASSERT_EQ_AT (loc, s.m_url[i], (cppchar_t)expected_str[i]);
809 8 : }
810 :
811 : #define ASSERT_STYLE_URLEQ(STYLE, EXPECTED_STR) \
812 : assert_style_urleq ((SELFTEST_LOCATION), (STYLE), (EXPECTED_STR))
813 :
814 : static void
815 4 : test_url ()
816 : {
817 : // URL_FORMAT_ST
818 4 : {
819 4 : style_manager sm;
820 4 : styled_string s
821 4 : (sm, "\33]8;;http://example.com\33\\This is a link\33]8;;\33\\");
822 4 : const char *expected = "This is a link";
823 4 : ASSERT_EQ (s.size (), strlen (expected));
824 4 : ASSERT_EQ (s.calc_canvas_width (), (int)strlen (expected));
825 4 : ASSERT_EQ (sm.get_num_styles (), 2);
826 60 : for (size_t i = 0; i < strlen (expected); i++)
827 : {
828 56 : ASSERT_EQ (s[i].get_code (), (cppchar_t)expected[i]);
829 56 : ASSERT_EQ (s[i].get_style_id (), 1);
830 : }
831 4 : ASSERT_STYLE_URLEQ (sm.get_style (1), "http://example.com");
832 4 : }
833 :
834 : // URL_FORMAT_BEL
835 4 : {
836 4 : style_manager sm;
837 4 : styled_string s
838 4 : (sm, "\33]8;;http://example.com\aThis is a link\33]8;;\a");
839 4 : const char *expected = "This is a link";
840 4 : ASSERT_EQ (s.size (), strlen (expected));
841 4 : ASSERT_EQ (s.calc_canvas_width (), (int)strlen (expected));
842 4 : ASSERT_EQ (sm.get_num_styles (), 2);
843 60 : for (size_t i = 0; i < strlen (expected); i++)
844 : {
845 56 : ASSERT_EQ (s[i].get_code (), (cppchar_t)expected[i]);
846 56 : ASSERT_EQ (s[i].get_style_id (), 1);
847 : }
848 4 : ASSERT_STYLE_URLEQ (sm.get_style (1), "http://example.com");
849 4 : }
850 4 : }
851 :
852 : static void
853 4 : test_from_fmt ()
854 : {
855 4 : style_manager sm;
856 4 : styled_string s (styled_string::from_fmt (sm, NULL, "%%i: %i", 42));
857 4 : ASSERT_EQ (s[0].get_code (), '%');
858 4 : ASSERT_EQ (s[1].get_code (), 'i');
859 4 : ASSERT_EQ (s[2].get_code (), ':');
860 4 : ASSERT_EQ (s[3].get_code (), ' ');
861 4 : ASSERT_EQ (s[4].get_code (), '4');
862 4 : ASSERT_EQ (s[5].get_code (), '2');
863 4 : ASSERT_EQ (s.size (), 6);
864 4 : ASSERT_EQ (s.calc_canvas_width (), 6);
865 4 : }
866 :
867 : static void
868 4 : test_from_fmt_qs ()
869 : {
870 4 : auto_fix_quotes fix_quotes;
871 4 : open_quote = "\xe2\x80\x98";
872 4 : close_quote = "\xe2\x80\x99";
873 :
874 4 : style_manager sm;
875 4 : styled_string s (styled_string::from_fmt (sm, NULL, "%qs", "msg"));
876 4 : ASSERT_EQ (sm.get_num_styles (), 2);
877 4 : ASSERT_EQ (s[0].get_code (), 0x2018);
878 4 : ASSERT_EQ (s[0].get_style_id (), 0);
879 4 : ASSERT_EQ (s[1].get_code (), 'm');
880 4 : ASSERT_EQ (s[1].get_style_id (), 1);
881 4 : ASSERT_EQ (s[2].get_code (), 's');
882 4 : ASSERT_EQ (s[2].get_style_id (), 1);
883 4 : ASSERT_EQ (s[3].get_code (), 'g');
884 4 : ASSERT_EQ (s[3].get_style_id (), 1);
885 4 : ASSERT_EQ (s[4].get_code (), 0x2019);
886 4 : ASSERT_EQ (s[4].get_style_id (), 0);
887 4 : ASSERT_EQ (s.size (), 5);
888 4 : }
889 :
890 : // Test of parsing SGR codes.
891 :
892 : static void
893 4 : test_from_str_with_bold ()
894 : {
895 4 : style_manager sm;
896 : /* This is the result of pp_printf (pp, "%qs", "foo")
897 : with auto_fix_quotes. */
898 4 : styled_string s (sm, "`\33[01m\33[Kfoo\33[m\33[K'");
899 4 : ASSERT_EQ (s[0].get_code (), '`');
900 4 : ASSERT_EQ (s[0].get_style_id (), 0);
901 4 : ASSERT_EQ (s[1].get_code (), 'f');
902 4 : ASSERT_EQ (s[1].get_style_id (), 1);
903 4 : ASSERT_EQ (s[2].get_code (), 'o');
904 4 : ASSERT_EQ (s[2].get_style_id (), 1);
905 4 : ASSERT_EQ (s[3].get_code (), 'o');
906 4 : ASSERT_EQ (s[3].get_style_id (), 1);
907 4 : ASSERT_EQ (s[4].get_code (), '\'');
908 4 : ASSERT_EQ (s[4].get_style_id (), 0);
909 4 : ASSERT_EQ (s.size (), 5);
910 4 : ASSERT_TRUE (sm.get_style (1).m_bold);
911 4 : }
912 :
913 : static void
914 4 : test_from_str_with_underscore ()
915 : {
916 4 : style_manager sm;
917 4 : styled_string s (sm, "\33[04m\33[KA");
918 4 : ASSERT_EQ (s[0].get_code (), 'A');
919 4 : ASSERT_EQ (s[0].get_style_id (), 1);
920 4 : ASSERT_TRUE (sm.get_style (1).m_underscore);
921 4 : }
922 :
923 : static void
924 4 : test_from_str_with_blink ()
925 : {
926 4 : style_manager sm;
927 4 : styled_string s (sm, "\33[05m\33[KA");
928 4 : ASSERT_EQ (s[0].get_code (), 'A');
929 4 : ASSERT_EQ (s[0].get_style_id (), 1);
930 4 : ASSERT_TRUE (sm.get_style (1).m_blink);
931 4 : }
932 :
933 : // Test of parsing SGR codes.
934 :
935 : static void
936 4 : test_from_str_with_color ()
937 : {
938 4 : style_manager sm;
939 :
940 4 : styled_string s (sm,
941 : ("0"
942 : SGR_SEQ (COLOR_FG_RED)
943 : "R"
944 : SGR_RESET
945 : "2"
946 : SGR_SEQ (COLOR_FG_GREEN)
947 : "G"
948 : SGR_RESET
949 4 : "4"));
950 4 : ASSERT_EQ (s.size (), 5);
951 4 : ASSERT_EQ (sm.get_num_styles (), 3);
952 4 : ASSERT_EQ (s[0].get_code (), '0');
953 4 : ASSERT_EQ (s[0].get_style_id (), 0);
954 4 : ASSERT_EQ (s[1].get_code (), 'R');
955 4 : ASSERT_EQ (s[1].get_style_id (), 1);
956 4 : ASSERT_EQ (s[2].get_code (), '2');
957 4 : ASSERT_EQ (s[2].get_style_id (), 0);
958 4 : ASSERT_EQ (s[3].get_code (), 'G');
959 4 : ASSERT_EQ (s[3].get_style_id (), 2);
960 4 : ASSERT_EQ (s[4].get_code (), '4');
961 4 : ASSERT_EQ (s[4].get_style_id (), 0);
962 4 : ASSERT_EQ (sm.get_style (1).m_fg_color, style::named_color::RED);
963 4 : ASSERT_EQ (sm.get_style (2).m_fg_color, style::named_color::GREEN);
964 4 : }
965 :
966 : static void
967 4 : test_from_str_with_named_color ()
968 : {
969 4 : style_manager sm;
970 4 : styled_string s (sm,
971 : ("F"
972 : SGR_SEQ (COLOR_FG_BLACK) "F"
973 : SGR_SEQ (COLOR_FG_RED) "F"
974 : SGR_SEQ (COLOR_FG_GREEN) "F"
975 : SGR_SEQ (COLOR_FG_YELLOW) "F"
976 : SGR_SEQ (COLOR_FG_BLUE) "F"
977 : SGR_SEQ (COLOR_FG_MAGENTA) "F"
978 : SGR_SEQ (COLOR_FG_CYAN) "F"
979 : SGR_SEQ (COLOR_FG_WHITE) "F"
980 : SGR_SEQ (COLOR_FG_BRIGHT_BLACK) "F"
981 : SGR_SEQ (COLOR_FG_BRIGHT_RED) "F"
982 : SGR_SEQ (COLOR_FG_BRIGHT_GREEN) "F"
983 : SGR_SEQ (COLOR_FG_BRIGHT_YELLOW) "F"
984 : SGR_SEQ (COLOR_FG_BRIGHT_BLUE) "F"
985 : SGR_SEQ (COLOR_FG_BRIGHT_MAGENTA) "F"
986 : SGR_SEQ (COLOR_FG_BRIGHT_CYAN) "F"
987 : SGR_SEQ (COLOR_FG_BRIGHT_WHITE) "F"
988 : SGR_SEQ (COLOR_BG_BLACK) "B"
989 : SGR_SEQ (COLOR_BG_RED) "B"
990 : SGR_SEQ (COLOR_BG_GREEN) "B"
991 : SGR_SEQ (COLOR_BG_YELLOW) "B"
992 : SGR_SEQ (COLOR_BG_BLUE) "B"
993 : SGR_SEQ (COLOR_BG_MAGENTA) "B"
994 : SGR_SEQ (COLOR_BG_CYAN) "B"
995 : SGR_SEQ (COLOR_BG_WHITE) "B"
996 : SGR_SEQ (COLOR_BG_BRIGHT_BLACK) "B"
997 : SGR_SEQ (COLOR_BG_BRIGHT_RED) "B"
998 : SGR_SEQ (COLOR_BG_BRIGHT_GREEN) "B"
999 : SGR_SEQ (COLOR_BG_BRIGHT_YELLOW) "B"
1000 : SGR_SEQ (COLOR_BG_BRIGHT_BLUE) "B"
1001 : SGR_SEQ (COLOR_BG_BRIGHT_MAGENTA) "B"
1002 : SGR_SEQ (COLOR_BG_BRIGHT_CYAN) "B"
1003 4 : SGR_SEQ (COLOR_BG_BRIGHT_WHITE) "B"));
1004 4 : ASSERT_EQ (s.size (), 33);
1005 136 : for (size_t i = 0; i < s.size (); i++)
1006 132 : ASSERT_EQ (s[i].get_style_id (), i);
1007 72 : for (size_t i = 0; i < 17; i++)
1008 68 : ASSERT_EQ (s[i].get_code (), 'F');
1009 68 : for (size_t i = 17; i < 33; i++)
1010 64 : ASSERT_EQ (s[i].get_code (), 'B');
1011 4 : }
1012 :
1013 : static void
1014 4 : test_from_str_with_8_bit_color ()
1015 : {
1016 4 : {
1017 4 : style_manager sm;
1018 4 : styled_string s (sm,
1019 4 : ("[38;5;232m[KF"));
1020 4 : ASSERT_EQ (s.size (), 1);
1021 4 : ASSERT_EQ (s[0].get_code (), 'F');
1022 4 : ASSERT_EQ (s[0].get_style_id (), 1);
1023 4 : ASSERT_EQ (sm.get_style (1).m_fg_color, style::color (232));
1024 4 : }
1025 4 : {
1026 4 : style_manager sm;
1027 4 : styled_string s (sm,
1028 4 : ("[48;5;231m[KB"));
1029 4 : ASSERT_EQ (s.size (), 1);
1030 4 : ASSERT_EQ (s[0].get_code (), 'B');
1031 4 : ASSERT_EQ (s[0].get_style_id (), 1);
1032 4 : ASSERT_EQ (sm.get_style (1).m_bg_color, style::color (231));
1033 4 : }
1034 4 : }
1035 :
1036 : static void
1037 4 : test_from_str_with_24_bit_color ()
1038 : {
1039 4 : {
1040 4 : style_manager sm;
1041 4 : styled_string s (sm,
1042 4 : ("[38;2;243;250;242m[KF"));
1043 4 : ASSERT_EQ (s.size (), 1);
1044 4 : ASSERT_EQ (s[0].get_code (), 'F');
1045 4 : ASSERT_EQ (s[0].get_style_id (), 1);
1046 4 : ASSERT_EQ (sm.get_style (1).m_fg_color, style::color (243, 250, 242));
1047 4 : }
1048 4 : {
1049 4 : style_manager sm;
1050 4 : styled_string s (sm,
1051 4 : ("[48;2;253;247;231m[KB"));
1052 4 : ASSERT_EQ (s.size (), 1);
1053 4 : ASSERT_EQ (s[0].get_code (), 'B');
1054 4 : ASSERT_EQ (s[0].get_style_id (), 1);
1055 4 : ASSERT_EQ (sm.get_style (1).m_bg_color, style::color (253, 247, 231));
1056 4 : }
1057 4 : }
1058 :
1059 : static void
1060 4 : test_from_str_combining_characters ()
1061 : {
1062 4 : style_manager sm;
1063 4 : styled_string s (sm,
1064 : /* CYRILLIC CAPITAL LETTER U (U+0423). */
1065 : "\xD0\xA3"
1066 : /* COMBINING BREVE (U+0306). */
1067 4 : "\xCC\x86");
1068 4 : ASSERT_EQ (s.size (), 1);
1069 4 : ASSERT_EQ (s[0].get_code (), 0x423);
1070 4 : ASSERT_EQ (s[0].get_combining_chars ().size (), 1);
1071 4 : ASSERT_EQ (s[0].get_combining_chars ()[0], 0x306);
1072 4 : }
1073 :
1074 : /* Run all selftests in this file. */
1075 :
1076 : void
1077 4 : text_art_styled_string_cc_tests ()
1078 : {
1079 4 : test_combining_chars ();
1080 4 : test_empty ();
1081 4 : test_simple ();
1082 4 : test_pi_from_utf8 ();
1083 4 : test_emoji_from_utf8 ();
1084 4 : test_emoji_variant_from_utf8 ();
1085 4 : test_emoji_from_codepoint ();
1086 4 : test_from_mixed_width_utf8 ();
1087 4 : test_url ();
1088 4 : test_from_fmt ();
1089 4 : test_from_fmt_qs ();
1090 4 : test_from_str_with_bold ();
1091 4 : test_from_str_with_underscore ();
1092 4 : test_from_str_with_blink ();
1093 4 : test_from_str_with_color ();
1094 4 : test_from_str_with_named_color ();
1095 4 : test_from_str_with_8_bit_color ();
1096 4 : test_from_str_with_24_bit_color ();
1097 4 : test_from_str_combining_characters ();
1098 4 : }
1099 :
1100 : } // namespace selftest
1101 :
1102 :
1103 : #endif /* #if CHECKING_P */
|