diff --git a/include/fmt/format.h b/include/fmt/format.h index bd55b9b2..d9d7319a 100644 --- a/include/fmt/format.h +++ b/include/fmt/format.h @@ -665,6 +665,25 @@ FMT_CONSTEXPR void for_each_codepoint(string_view s, F f) { } while (buf_ptr < buf + num_chars_left); } +template ::value)> +FMT_CONSTEXPR void for_each_codepoint(basic_string_view s, F f) { + auto p = s.begin(), end = s.end(); + while (p != end) { + auto begin = p; + uint32_t cp = static_cast>(*p++); + // Leave unpaired surrogates intact so they can be escaped individually. + if (sizeof(Char) == 2 && cp >= 0xd800 && cp <= 0xdbff && p != end) { + uint32_t low = static_cast>(*p); + if (low >= 0xdc00 && low <= 0xdfff) { + cp = 0x10000 + ((cp - 0xd800) << 10) + (low - 0xdc00); + ++p; + } + } + if (!f(cp, basic_string_view(begin, to_unsigned(p - begin)))) return; + } +} + struct wide_cp_range { uint32_t first; uint32_t last; @@ -1982,12 +2001,17 @@ template struct find_escape_result { template auto find_escape(const Char* begin, const Char* end) -> find_escape_result { - for (; begin != end; ++begin) { - uint32_t cp = static_cast>(*begin); - if (sizeof(Char) == 1 && cp >= 0x80) continue; - if (needs_escape(cp)) return {begin, begin + 1, cp}; - } - return {begin, nullptr, 0}; + auto result = find_escape_result{end, nullptr, 0}; + for_each_codepoint(basic_string_view(begin, to_unsigned(end - begin)), + [&](uint32_t cp, basic_string_view sv) { + if (sizeof(Char) == 1 && cp >= 0x80) return true; + if (needs_escape(cp)) { + result = {sv.begin(), sv.end(), cp}; + return false; + } + return true; + }); + return result; } inline auto find_escape(const char* begin, const char* end) @@ -2406,14 +2430,13 @@ FMT_CONSTEXPR auto write(OutputIt out, Char value, const format_specs& specs, : write(out, static_cast(value), specs, loc); } -template ::value)> +template FMT_CONSTEXPR auto write(OutputIt out, basic_string_view s, const format_specs& specs) -> OutputIt { bool is_debug = specs.type() == presentation_type::debug; if (specs.precision < 0 && specs.width == 0) { auto&& it = reserve(out, s.size()); - return is_debug ? write_escaped_string(it, s) : copy(s, it); + return is_debug ? write_escaped_string(it, s) : copy(s, it); } size_t display_width_limit = @@ -2421,12 +2444,13 @@ FMT_CONSTEXPR auto write(OutputIt out, basic_string_view s, size_t display_width = !is_debug || specs.precision == 0 ? 0 : 1; // Account for opening '"'. size_t size = !is_debug || specs.precision == 0 ? 0 : 1; - for_each_codepoint(s, [&](uint32_t cp, string_view sv) { - if (is_debug && needs_escape(cp)) { - counting_buffer buf; - write_escaped_cp(basic_appender(buf), - find_escape_result{sv.begin(), sv.end(), cp}); - // We're reinterpreting bytes as display width. That's okay + for_each_codepoint(s, [&](uint32_t cp, basic_string_view sv) { + if (is_debug && needs_escape(cp) && + (sizeof(Char) != 1 || std::is_same::value || cp < 0x80)) { + counting_buffer buf; + write_escaped_cp(basic_appender(buf), + find_escape_result{sv.begin(), sv.end(), cp}); + // We're reinterpreting code units as display width. That's okay // because write_escaped_cp() only writes ASCII characters. size_t cp_width = buf.count(); if (display_width + cp_width <= display_width_limit) { @@ -2478,7 +2502,7 @@ FMT_CONSTEXPR auto write(OutputIt out, basic_string_view s, FMT_CONSTEXPR auto operator++(int) -> bounded_output_iterator& { return *this; } - FMT_CONSTEXPR auto operator=(char c) -> bounded_output_iterator& { + FMT_CONSTEXPR auto operator=(Char c) -> bounded_output_iterator& { if (bound > 0) { *underlying_iterator++ = c; --bound; @@ -2487,35 +2511,12 @@ FMT_CONSTEXPR auto write(OutputIt out, basic_string_view s, } }; - return write_padded( + return write_padded( out, specs, size, display_width, [=](reserve_iterator it) { return is_debug ? write_escaped_string(bounded_output_iterator{it, size}, s) .underlying_iterator - : copy(s.data(), s.data() + size, it); - }); -} - -template ::value)> -FMT_CONSTEXPR auto write(OutputIt out, basic_string_view s, - const format_specs& specs) -> OutputIt { - auto data = s.data(); - auto size = s.size(); - if (specs.precision >= 0 && to_unsigned(specs.precision) < size) - size = to_unsigned(specs.precision); - - bool is_debug = specs.type() == presentation_type::debug; - if (is_debug) { - auto buf = counting_buffer(); - write_escaped_string(basic_appender(buf), s); - size = buf.count(); - } - - return write_padded( - out, specs, size, [=](reserve_iterator it) { - return is_debug ? write_escaped_string(it, s) - : copy(data, data + size, it); + : copy(s.data(), s.data() + size, it); }); } diff --git a/test/xchar-test.cc b/test/xchar-test.cc index 6fc174ef..1dcbe0e8 100644 --- a/test/xchar-test.cc +++ b/test/xchar-test.cc @@ -298,6 +298,98 @@ TEST(xchar_test, escape_string) { EXPECT_EQ(fmt::format(L"{}", vec{L"понедельник"}), L"[\"понедельник\"]"); } +TEST(xchar_test, utf16_string_width) { + EXPECT_EQ(fmt::format(u"{:>4}", u"\u4e2d"), u" \u4e2d"); + EXPECT_EQ(fmt::format(u"{:^5}", u"\U0001f600"), u" \U0001f600 "); + EXPECT_EQ(fmt::format(u"{:>{}}", u"\U00010000", 3), u" \U00010000"); + EXPECT_EQ(fmt::format(u"{:<5}", u"a\U0001f600"), u"a\U0001f600 "); +} + +TEST(xchar_test, utf16_string_precision) { + EXPECT_EQ(fmt::format(u"{:.1}", u"\U00010000x"), u"\U00010000"); + EXPECT_EQ(fmt::format(u"{:.1}", u"\U0001f600x"), u""); + EXPECT_EQ(fmt::format(u"{:.2}", u"\U0001f600x"), u"\U0001f600"); + EXPECT_EQ(fmt::format(u"{:.3}", u"a\U0001f600x"), u"a\U0001f600"); + EXPECT_EQ(fmt::format(u"{:>4.2}", u"\U0001f600x"), u" \U0001f600"); + EXPECT_EQ(fmt::format(u"{:.0}", u"\U0001f600"), u""); + const char16_t input[] = {u'a', 0, 0xd800, 0xdc00}; + auto view = fmt::basic_string_view(input, 4); + EXPECT_EQ(fmt::format(u"{:.3}", view), std::u16string(input, 4)); +} + +TEST(xchar_test, utf16_debug_string) { + EXPECT_EQ(fmt::format(u"{:?}", u"\U0001f600"), u"\"\U0001f600\""); + EXPECT_EQ(fmt::format(u"{:?}", u"a\U00010000\n\U0001f600"), + u"\"a\U00010000\\n\U0001f600\""); + EXPECT_EQ(fmt::format(u"{:?}", u"\U0001d173"), u"\"\\U0001d173\""); + EXPECT_EQ(fmt::format(u"{:?}", u"\U0010ffff"), u"\"\\U0010ffff\""); + auto values = std::vector{u"\U0001f600", u"\U0001d173"}; + EXPECT_EQ(fmt::format(u"{}", values), u"[\"\U0001f600\", \"\\U0001d173\"]"); +} + +TEST(xchar_test, utf16_debug_width_and_precision) { + EXPECT_EQ(fmt::format(u"{:>6?}", u"\U0001f600"), u" \"\U0001f600\""); + EXPECT_EQ(fmt::format(u"{:.0?}", u"\U0001f600"), u""); + EXPECT_EQ(fmt::format(u"{:.1?}", u"\U0001f600"), u"\""); + EXPECT_EQ(fmt::format(u"{:.2?}", u"\U0001f600"), u"\""); + EXPECT_EQ(fmt::format(u"{:.3?}", u"\U0001f600"), u"\"\U0001f600"); + EXPECT_EQ(fmt::format(u"{:.4?}", u"\U0001f600"), u"\"\U0001f600\""); + EXPECT_EQ(fmt::format(u"{:>6.3?}", u"\U0001f600"), u" \"\U0001f600"); + EXPECT_EQ(fmt::format(u"{:.4?}", u"\U0001d173"), u"\"\\U0"); + EXPECT_EQ(fmt::format(u"{:.1?}", u""), u"\""); + EXPECT_EQ(fmt::format(u"{:.2?}", u""), u"\"\""); +} + +TEST(xchar_test, utf16_debug_invalid_sequences) { + const char16_t high[] = {0xd800, 0}; + const char16_t low[] = {0xdc00, 0}; + const char16_t high_ascii[] = {0xd800, u'x', 0}; + const char16_t high_pair[] = {0xd800, 0xd800, 0xdc00, 0}; + EXPECT_EQ(fmt::format(u"{:?}", high), u"\"\\ud800\""); + EXPECT_EQ(fmt::format(u"{:?}", low), u"\"\\udc00\""); + EXPECT_EQ(fmt::format(u"{:?}", high_ascii), u"\"\\ud800x\""); + EXPECT_EQ(fmt::format(u"{:?}", high_pair), u"\"\\ud800\U00010000\""); + EXPECT_EQ(fmt::format(u"{:.1}", high_pair), std::u16string(high_pair, 1)); +} + +TEST(xchar_test, utf32_string_width_and_precision) { + EXPECT_EQ(fmt::format(U"{:>4}", U"\u4e2d"), U" \u4e2d"); + EXPECT_EQ(fmt::format(U"{:^5}", U"\U0001f600"), U" \U0001f600 "); + EXPECT_EQ(fmt::format(U"{:.1}", U"\U0001f600x"), U""); + EXPECT_EQ(fmt::format(U"{:.2}", U"\U0001f600x"), U"\U0001f600"); + EXPECT_EQ(fmt::format(U"{:>6?}", U"\U0001f600"), U" \"\U0001f600\""); + EXPECT_EQ(fmt::format(U"{:.4?}", U"\U0001d173"), U"\"\\U0"); +} + +TEST(xchar_test, wide_string_width_and_precision) { + EXPECT_EQ(fmt::format(L"{:>4}", L"\u4e2d"), L" \u4e2d"); + EXPECT_EQ(fmt::format(L"{:.1}", L"\U0001f600x"), L""); + EXPECT_EQ(fmt::format(L"{:.2}", L"\U0001f600x"), L"\U0001f600"); + EXPECT_EQ(fmt::format(L"{:?}", L"\U0001f600"), L"\"\U0001f600\""); + EXPECT_EQ(fmt::format(L"{:>6?}", L"\U0001f600"), L" \"\U0001f600\""); + EXPECT_EQ(fmt::format(L"{:.4?}", L"\U0001d173"), L"\"\\U0"); +} + +TEST(xchar_test, utf16_format_to) { + auto output = std::u16string(); + fmt::format_to(std::back_inserter(output), u"{:>{}.{}}", u"\U0001f600x", 4, + 2); + EXPECT_EQ(output, u" \U0001f600"); + char16_t buffer[4] = {}; + auto result = fmt::format_to_n(buffer, 4, u"{:>6?}", u"\U0001f600"); + EXPECT_EQ(std::u16string(buffer, 4), u" \"\xd83d"); + EXPECT_EQ(result.size, 6); + EXPECT_EQ(result.out, buffer + 4); +} + +#ifdef __cpp_char8_t +TEST(xchar_test, char8_debug_string_width) { + EXPECT_EQ(fmt::format(u8"{:>7?}", u8"\u4e2d"), u8" \"\u4e2d\""); + EXPECT_EQ(fmt::format(u8"{:>8?}", u8"\U0001f600"), u8" \"\U0001f600\""); + EXPECT_EQ(fmt::format(u8"{:>6?}", u8"\n"), u8" \"\\n\""); +} +#endif + TEST(xchar_test, to_wstring) { EXPECT_EQ(L"42", fmt::to_wstring(42)); } #ifndef FMT_STATIC_THOUSANDS_SEPARATOR