FTXUI 7.1.0
C++ functional terminal UI.
Loading...
Searching...
No Matches
string.cpp
Go to the documentation of this file.
1// Copyright 2020 Arthur Sonzogni. All rights reserved.
2// Use of this source code is governed by the MIT license that can be found in
3// the LICENSE file.
4//
5// Content of this file was created thanks to:
6// -
7// https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/WordBreakProperty.txt
8// - Markus Kuhn -- 2007-05-26 (Unicode 5.0)
9// http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
10// Thanks you!
11
13
14#include <array> // for array
15#include <cstddef> // for size_t
16#include <cstdint> // for uint32_t, uint8_t, uint16_t, int32_t
17#include <string> // for string, basic_string, wstring
18#include <string_view> // for string_view
19#include <tuple> // for _Swallow_assign, ignore
20#include <vector>
21
22#include "ftxui/screen/deprecated.hpp" // for wchar_width, wstring_width
23#include "ftxui/screen/string_internal.hpp" // for WordBreakProperty, EatCodePoint, CodepointToWordBreakProperty, GlyphCount, GlyphIterate, GlyphNext, GlyphPrevious, IsCombining, IsControl, IsFullWidth, Utf8ToWordBreakProperty
24
25namespace {
26
27struct Interval {
28 uint32_t first;
29 uint32_t last;
30};
31
32using WBP = ftxui::WordBreakProperty;
33struct WordBreakPropertyInterval {
34 uint32_t first;
35 uint32_t last;
36 WBP property;
37};
38
39// g_full_width_characters and g_word_break_intervals, generated from the
40// Unicode Character Database by tools/gen_unicode_tables.py.
42
43// Construct table of just WBP::Extend character intervals
44constexpr auto g_extend_characters{[]() constexpr {
45 // Compute number of extend character intervals
46 constexpr size_t size = []() constexpr {
47 size_t count = 0;
48 for (auto interval : g_word_break_intervals) {
49 if (interval.property == WBP::Extend) {
50 count++;
51 }
52 }
53 return count;
54 }();
55
56 // Create array of extend character intervals
57 std::array<Interval, size> result{};
58 size_t index = 0;
59 for (auto interval : g_word_break_intervals) {
60 if (interval.property == WBP::Extend) {
61 result[index++] = {interval.first, interval.last}; // NOLINT
62 }
63 }
64 return result;
65}()};
66
67// Find a codepoint inside a sorted list of Interval.
68template <size_t N>
69bool Bisearch(uint32_t ucs, const std::array<Interval, N>& table) {
70 if (ucs < table.front().first || ucs > table.back().last) { // NOLINT
71 return false;
72 }
73
74 int min = 0;
75 int max = N - 1;
76 while (max >= min) {
77 const int mid = (min + max) / 2;
78 if (ucs > table[mid].last) { // NOLINT
79 min = mid + 1;
80 } else if (ucs < table[mid].first) { // NOLINT
81 max = mid - 1;
82 } else {
83 return true;
84 }
85 }
86
87 return false;
88}
89
90// Find a value inside a sorted list of Interval + property.
91template <class C, size_t N>
92bool Bisearch(uint32_t ucs, const std::array<C, N>& table, C* out) {
93 if (ucs < table.front().first || ucs > table.back().last) { // NOLINT
94 return false;
95 }
96
97 int min = 0;
98 int max = N - 1;
99 while (max >= min) {
100 const int mid = (min + max) / 2;
101 if (ucs > table[mid].last) { // NOLINT
102 min = mid + 1;
103 } else if (ucs < table[mid].first) { // NOLINT
104 max = mid - 1;
105 } else {
106 *out = table[mid]; // NOLINT
107 return true;
108 }
109 }
110
111 return false;
112}
113
114// VARIATION SELECTOR-16 requests the emoji presentation of the preceding
115// character. Terminals draw it two cells wide.
116constexpr uint32_t kEmojiPresentationSelector = 0xFE0F;
117
118int codepoint_width(uint32_t ucs) {
119 if (ftxui::IsControl(ucs)) {
120 return -1;
121 }
122
123 if (ftxui::IsCombining(ucs)) {
124 return 0;
125 }
126
127 if (ftxui::IsFullWidth(ucs)) {
128 return 2;
129 }
130
131 return 1;
132}
133
134} // namespace
135
136namespace ftxui {
137
138// From UTF8 encoded string |input|, eat in between 1 and 4 byte representing
139// one codepoint. Put the codepoint into |ucs|. Start at |start| and update
140// |end| to represent the beginning of the next byte to eat for consecutive
141// executions.
142bool EatCodePoint(std::string_view input,
143 size_t start,
144 size_t* end,
145 uint32_t* ucs) {
146 if (start >= input.size()) {
147 *end = start + 1;
148 return false;
149 }
150 const uint8_t C0 = input[start];
151
152 // 1 byte string.
153 if ((C0 & 0b1000'0000) == 0b0000'0000) { // NOLINT
154 *ucs = C0 & 0b0111'1111; // NOLINT
155 *end = start + 1;
156 return true;
157 }
158
159 // 2 byte string.
160 if ((C0 & 0b1110'0000) == 0b1100'0000 && // NOLINT
161 start + 1 < input.size()) {
162 const uint8_t C1 = input[start + 1];
163 *ucs = 0;
164 *ucs += C0 & 0b0001'1111; // NOLINT
165 *ucs <<= 6; // NOLINT
166 *ucs += C1 & 0b0011'1111; // NOLINT
167 *end = start + 2;
168 return true;
169 }
170
171 // 3 byte string.
172 if ((C0 & 0b1111'0000) == 0b1110'0000 && // NOLINT
173 start + 2 < input.size()) {
174 const uint8_t C1 = input[start + 1];
175 const uint8_t C2 = input[start + 2];
176 *ucs = 0;
177 *ucs += C0 & 0b0000'1111; // NOLINT
178 *ucs <<= 6; // NOLINT
179 *ucs += C1 & 0b0011'1111; // NOLINT
180 *ucs <<= 6; // NOLINT
181 *ucs += C2 & 0b0011'1111; // NOLINT
182 *end = start + 3;
183 return true;
184 }
185
186 // 4 byte string.
187 if ((C0 & 0b1111'1000) == 0b1111'0000 && // NOLINT
188 start + 3 < input.size()) {
189 const uint8_t C1 = input[start + 1];
190 const uint8_t C2 = input[start + 2];
191 const uint8_t C3 = input[start + 3];
192 *ucs = 0;
193 *ucs += C0 & 0b0000'0111; // NOLINT
194 *ucs <<= 6; // NOLINT
195 *ucs += C1 & 0b0011'1111; // NOLINT
196 *ucs <<= 6; // NOLINT
197 *ucs += C2 & 0b0011'1111; // NOLINT
198 *ucs <<= 6; // NOLINT
199 *ucs += C3 & 0b0011'1111; // NOLINT
200 *end = start + 4;
201 return true;
202 }
203
204 *end = start + 1;
205 return false;
206}
207
208// From UTF16 encoded string |input|, eat in between 1 and 4 byte representing
209// one codepoint. Put the codepoint into |ucs|. Start at |start| and update
210// |end| to represent the beginning of the next byte to eat for consecutive
211// executions.
212bool EatCodePoint(std::wstring_view input,
213 size_t start,
214 size_t* end,
215 uint32_t* ucs) {
216 if (start >= input.size()) {
217 *end = start + 1;
218 return false;
219 }
220
221 // On linux wstring uses the UTF32 encoding:
222 if constexpr (sizeof(wchar_t) == 4) {
223 *ucs = input[start]; // NOLINT
224 *end = start + 1;
225 return true;
226 }
227
228 // On windows, wstring uses the UTF16 encoding:
229 int32_t C0 = input[start]; // NOLINT
230
231 // 1 word size:
232 if (C0 < 0xd800 || C0 >= 0xdc00) { // NOLINT
233 *ucs = C0;
234 *end = start + 1;
235 return true;
236 }
237
238 // 2 word size:
239 if (start + 1 >= input.size()) {
240 *end = start + 2;
241 return false;
242 }
243
244 int32_t C1 = input[start + 1]; // NOLINT
245 *ucs = ((C0 & 0x3ff) << 10) + (C1 & 0x3ff) + 0x10000; // NOLINT
246 *end = start + 2;
247 return true;
248}
249
250bool IsCombining(uint32_t ucs) {
251 return Bisearch(ucs, g_extend_characters);
252}
253
254bool IsFullWidth(uint32_t ucs) {
255 if (ucs < 0x0300) { // Quick path: // NOLINT
256 return false;
257 }
258
259 return Bisearch(ucs, g_full_width_characters);
260}
261
262bool IsControl(uint32_t ucs) {
263 if (ucs == 0) {
264 return true;
265 }
266 if (ucs < 32) { // NOLINT
267 const uint32_t LINE_FEED = 10;
268 return ucs != LINE_FEED;
269 }
270 if (ucs >= 0x7f && ucs < 0xa0) { // NOLINT
271 return true;
272 }
273 return false;
274}
275
277 WordBreakPropertyInterval interval = {0, 0, WBP::ALetter};
278 std::ignore = Bisearch(codepoint, g_word_break_intervals, &interval);
279 return interval.property;
280}
281
282int wchar_width(wchar_t ucs) {
283 return codepoint_width(uint32_t(ucs));
284}
285
286int wstring_width(const std::wstring& text) {
287 int width = 0;
288
289 for (const wchar_t& it : text) {
290 const int w = wchar_width(it);
291 if (w < 0) {
292 return -1;
293 }
294 width += w;
295 }
296 return width;
297}
298
299// Return how many cells the UTF8 encoded string |input| is taking when printed.
300// Control characters are not taking any space, combining characters are
301// modifying the previous character and are not taking any space, fullwidth
302// characters are taking two cells and all the other characters are taking one
303// cell.
304int string_width(std::string_view input) {
305 // 1-byte optimization: This function is often called on a single ASCII
306 // character, so we can optimize this case by skipping the UTF8 decoding.
307 if (input.size() == 1) {
308 const char c = input[0];
309 if (c >= 32 && c < 127) { // NOLINT
310 return 1;
311 }
312 }
313
314 // ASCII optimization: If the string is pure ASCII, we can skip the UTF8
315 // decoding and just count the number of characters, ignoring control
316 // characters.
317 bool is_pure_ascii = true;
318 for (const char c : input) {
319 if (c < 31 || c >= 127) { // NOLINT
320 is_pure_ascii = false;
321 break;
322 }
323 }
324 if (is_pure_ascii) {
325 return static_cast<int>(input.size());
326 }
327
328 int width = 0;
329 bool previous_narrow = false;
330 size_t start = 0;
331 while (start < input.size()) {
332 uint32_t codepoint = 0;
333 if (!EatCodePoint(input, start, &start, &codepoint)) {
334 continue;
335 }
336
337 if (IsControl(codepoint)) {
338 continue;
339 }
340
341 if (IsCombining(codepoint)) {
342 if (codepoint == kEmojiPresentationSelector && previous_narrow) {
343 previous_narrow = false;
344 width += 1;
345 }
346 continue;
347 }
348
349 if (IsFullWidth(codepoint)) {
350 previous_narrow = false;
351 width += 2;
352 continue;
353 }
354
355 previous_narrow = true;
356 width += 1;
357 }
358 return width;
359}
360
361std::vector<std::string> Utf8ToGlyphs(std::string_view input) {
362 std::vector<std::string> out;
363 out.reserve(input.size());
364 size_t last_glyph = 0;
365 bool last_glyph_narrow = false;
366 size_t start = 0;
367 size_t end = 0;
368 while (start < input.size()) {
369 uint32_t codepoint = 0;
370 if (!EatCodePoint(input, start, &end, &codepoint)) {
371 start = end;
372 continue;
373 }
374
375 const auto append = input.substr(start, end - start);
376 start = end;
377
378 // Ignore control characters.
379 if (IsControl(codepoint)) {
380 continue;
381 }
382
383 // Combining characters are put with the previous glyph they are modifying.
384 if (IsCombining(codepoint)) {
385 if (!out.empty()) {
386 out[last_glyph] += append;
387 }
388 // The emoji presentation makes a narrow glyph take two cells.
389 if (codepoint == kEmojiPresentationSelector && last_glyph_narrow) {
390 last_glyph_narrow = false;
391 out.emplace_back("");
392 }
393 continue;
394 }
395
396 last_glyph = out.size();
397
398 // Fullwidth characters take two cells. The second is made of the empty
399 // string to reserve the space the first is taking.
400 if (IsFullWidth(codepoint)) {
401 last_glyph_narrow = false;
402 out.emplace_back(append);
403 out.emplace_back("");
404 continue;
405 }
406
407 // Normal characters:
408 last_glyph_narrow = true;
409 out.emplace_back(append);
410 }
411 return out;
412}
413
414size_t GlyphPrevious(std::string_view input, size_t start) {
415 while (true) {
416 if (start == 0) {
417 return 0;
418 }
419 start--;
420
421 // Skip the UTF8 continuation bytes.
422 if ((input[start] & 0b1100'0000) == 0b1000'0000) {
423 continue;
424 }
425
426 uint32_t codepoint = 0;
427 size_t end = 0;
428 const bool eaten = EatCodePoint(input, start, &end, &codepoint);
429
430 // Ignore invalid, control characters and combining characters.
431 if (!eaten || IsControl(codepoint) || IsCombining(codepoint)) {
432 continue;
433 }
434
435 return start;
436 }
437}
438
439size_t GlyphNext(std::string_view input, size_t start) {
440 bool glyph_found = false;
441 while (start < input.size()) {
442 size_t end = 0;
443 uint32_t codepoint = 0;
444 const bool eaten = EatCodePoint(input, start, &end, &codepoint);
445
446 // Ignore invalid, control characters and combining characters.
447 if (!eaten || IsControl(codepoint) || IsCombining(codepoint)) {
448 start = end;
449 continue;
450 }
451
452 // We eat the beginning of the next glyph. If we are eating the one
453 // requested, return its start position immediately.
454 if (glyph_found) {
455 return static_cast<int>(start);
456 }
457
458 // Otherwise, skip this glyph and iterate:
459 glyph_found = true;
460 start = end;
461 }
462 return static_cast<int>(input.size());
463}
464
465size_t GlyphIterate(std::string_view input, int glyph_offset, size_t start) {
466 if (glyph_offset >= 0) {
467 for (int i = 0; i < glyph_offset; ++i) {
468 start = GlyphNext(input, start);
469 }
470 return start;
471 } else {
472 for (int i = 0; i < -glyph_offset; ++i) {
473 start = GlyphPrevious(input, start);
474 }
475 return start;
476 }
477}
478
479std::vector<int> CellToGlyphIndex(std::string_view input) {
480 int x = -1;
481 bool last_glyph_narrow = false;
482 std::vector<int> out;
483 out.reserve(input.size());
484 size_t start = 0;
485 size_t end = 0;
486 while (start < input.size()) {
487 uint32_t codepoint = 0;
488 const bool eaten = EatCodePoint(input, start, &end, &codepoint);
489 start = end;
490
491 // Ignore invalid / control characters.
492 if (!eaten || IsControl(codepoint)) {
493 continue;
494 }
495
496 // Combining characters are put with the previous glyph they are modifying.
497 if (IsCombining(codepoint)) {
498 if (x == -1) {
499 ++x;
500 out.push_back(x);
501 }
502 // The emoji presentation makes a narrow glyph take two cells.
503 if (codepoint == kEmojiPresentationSelector && last_glyph_narrow) {
504 last_glyph_narrow = false;
505 out.push_back(x);
506 }
507 continue;
508 }
509
510 // Fullwidth characters take two cells. The second is made of the empty
511 // string to reserve the space the first is taking.
512 if (IsFullWidth(codepoint)) {
513 last_glyph_narrow = false;
514 ++x;
515 out.push_back(x);
516 out.push_back(x);
517 continue;
518 }
519
520 // Normal characters:
521 last_glyph_narrow = true;
522 ++x;
523 out.push_back(x);
524 }
525 return out;
526}
527
528int GlyphCount(std::string_view input) {
529 int size = 0;
530 size_t start = 0;
531 size_t end = 0;
532 while (start < input.size()) {
533 uint32_t codepoint = 0;
534 const bool eaten = EatCodePoint(input, start, &end, &codepoint);
535 start = end;
536
537 // Ignore invalid characters:
538 if (!eaten || IsControl(codepoint)) {
539 continue;
540 }
541
542 // Ignore combining characters, except when they don't have a preceding to
543 // combine with.
544 if (IsCombining(codepoint)) {
545 if (size == 0) {
546 size++;
547 }
548 continue;
549 }
550
551 size++;
552 }
553 return size;
554}
555
556std::vector<WordBreakProperty> Utf8ToWordBreakProperty(std::string_view input) {
557 std::vector<WordBreakProperty> out;
558 out.reserve(input.size());
559 size_t start = 0;
560 size_t end = 0;
561 while (start < input.size()) {
562 uint32_t codepoint = 0;
563 if (!EatCodePoint(input, start, &end, &codepoint)) {
564 start = end;
565 continue;
566 }
567 start = end;
568
569 // Ignore control characters.
570 if (IsControl(codepoint)) {
571 continue;
572 }
573
574 // Ignore combining characters.
575 if (IsCombining(codepoint)) {
576 continue;
577 }
578
579 WordBreakPropertyInterval interval = {0, 0, WBP::ALetter};
580 std::ignore = Bisearch(codepoint, g_word_break_intervals, &interval);
581 out.push_back(interval.property);
582 }
583 return out;
584}
585
586/// Convert a std::wstring into a UTF8 std::string.
587std::string to_string(std::wstring_view s) {
588 std::string out;
589
590 size_t i = 0;
591 uint32_t codepoint = 0;
592 while (EatCodePoint(s, i, &i, &codepoint)) {
593 // Code point <-> UTF-8 conversion
594 //
595 // ┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
596 // ┃Byte 1 ┃Byte 2 ┃Byte 3 ┃Byte 4 ┃
597 // ┡━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
598 // │0xxxxxxx│ │ │ │
599 // ├────────┼────────┼────────┼────────┤
600 // │110xxxxx│10xxxxxx│ │ │
601 // ├────────┼────────┼────────┼────────┤
602 // │1110xxxx│10xxxxxx│10xxxxxx│ │
603 // ├────────┼────────┼────────┼────────┤
604 // │11110xxx│10xxxxxx│10xxxxxx│10xxxxxx│
605 // └────────┴────────┴────────┴────────┘
606
607 // 1 byte UTF8
608 if (codepoint <= 0b000'0000'0111'1111) { // NOLINT
609 const uint8_t p1 = codepoint;
610 out.push_back(p1); // NOLINT
611 continue;
612 }
613
614 // 2 bytes UTF8
615 if (codepoint <= 0b000'0111'1111'1111) { // NOLINT
616 uint8_t p2 = codepoint & 0b111111; // NOLINT
617 codepoint >>= 6; // NOLINT
618 uint8_t p1 = codepoint; // NOLINT
619 out.push_back(0b11000000 + p1); // NOLINT
620 out.push_back(0b10000000 + p2); // NOLINT
621 continue;
622 }
623
624 // 3 bytes UTF8
625 if (codepoint <= 0b1111'1111'1111'1111) { // NOLINT
626 uint8_t p3 = codepoint & 0b111111; // NOLINT
627 codepoint >>= 6; // NOLINT
628 uint8_t p2 = codepoint & 0b111111; // NOLINT
629 codepoint >>= 6; // NOLINT
630 uint8_t p1 = codepoint; // NOLINT
631 out.push_back(0b11100000 + p1); // NOLINT
632 out.push_back(0b10000000 + p2); // NOLINT
633 out.push_back(0b10000000 + p3); // NOLINT
634 continue;
635 }
636
637 // 4 bytes UTF8
638 if (codepoint <= 0b1'0000'1111'1111'1111'1111) { // NOLINT
639 uint8_t p4 = codepoint & 0b111111; // NOLINT
640 codepoint >>= 6; // NOLINT
641 uint8_t p3 = codepoint & 0b111111; // NOLINT
642 codepoint >>= 6; // NOLINT
643 uint8_t p2 = codepoint & 0b111111; // NOLINT
644 codepoint >>= 6; // NOLINT
645 uint8_t p1 = codepoint; // NOLINT
646 out.push_back(0b11110000 + p1); // NOLINT
647 out.push_back(0b10000000 + p2); // NOLINT
648 out.push_back(0b10000000 + p3); // NOLINT
649 out.push_back(0b10000000 + p4); // NOLINT
650 continue;
651 }
652
653 // Something else?
654 }
655 return out;
656}
657
658/// Convert a UTF8 std::string into a std::wstring.
659std::wstring to_wstring(std::string_view s) {
660 std::wstring out;
661
662 size_t i = 0;
663 uint32_t codepoint = 0;
664 while (EatCodePoint(s, i, &i, &codepoint)) {
665 // On linux wstring are UTF32 encoded:
666 if constexpr (sizeof(wchar_t) == 4) {
667 out.push_back(codepoint); // NOLINT
668 continue;
669 }
670
671 // On Windows, wstring are UTF16 encoded:
672
673 // Codepoint encoded using 1 word:
674 // NOLINTNEXTLINE
675 if (codepoint < 0xD800 || (codepoint > 0xDFFF && codepoint < 0x10000)) {
676 uint16_t p0 = codepoint; // NOLINT
677 out.push_back(p0); // NOLINT
678 continue;
679 }
680
681 // Codepoint encoded using 2 words:
682 codepoint -= 0x010000; // NOLINT
683 uint16_t p0 = (((codepoint << 12) >> 22) + 0xD800); // NOLINT
684 uint16_t p1 = (((codepoint << 22) >> 22) + 0xDC00); // NOLINT
685 out.push_back(p0); // NOLINT
686 out.push_back(p1); // NOLINT
687 }
688 return out;
689}
690
691} // namespace ftxui
The FTXUI ftxui:: namespace.
Definition animation.hpp:11
bool IsControl(uint32_t ucs)
Definition string.cpp:262
WordBreakProperty CodepointToWordBreakProperty(uint32_t codepoint)
Definition string.cpp:276
size_t GlyphPrevious(std::string_view input, size_t start)
Definition string.cpp:414
FTXUI_EXPORT(SCREEN) int string_width(std std::vector< std::string > Utf8ToGlyphs(std::string_view input)
Definition string.cpp:361
int string_width(std::string_view input)
Definition string.cpp:304
bool IsCombining(uint32_t ucs)
Definition string.cpp:250
int wchar_width(wchar_t ucs)
Definition string.cpp:282
bool EatCodePoint(std::string_view input, size_t start, size_t *end, uint32_t *ucs)
Definition string.cpp:142
std::string to_string(std::wstring_view s)
Convert a std::wstring into a UTF8 std::string.
Definition string.cpp:587
int GlyphCount(std::string_view input)
Definition string.cpp:528
std::vector< WordBreakProperty > Utf8ToWordBreakProperty(std::string_view input)
Definition string.cpp:556
int wstring_width(const std::wstring &text)
Definition string.cpp:286
std::vector< int > CellToGlyphIndex(std::string_view input)
Definition string.cpp:479
FTXUI_EXPORT(SCREEN) std FTXUI_EXPORT(SCREEN) std std::wstring to_wstring(T s)
Definition string.hpp:18
size_t GlyphIterate(std::string_view input, int glyph_offset, size_t start)
Definition string.cpp:465
bool IsFullWidth(uint32_t ucs)
Definition string.cpp:254
size_t GlyphNext(std::string_view input, size_t start)
Definition string.cpp:439
size_t size
Definition screen.cpp:82
constexpr std::array< Interval, 126 > g_full_width_characters
constexpr std::array< WordBreakPropertyInterval, 1114 > g_word_break_intervals