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