Fix Unicode width, precision and escaping for wide strings

This commit is contained in:
tianrking 2026-10-05 02:15:58 +08:00
parent 4afd0e498e
commit 03c047a2ec
2 changed files with 134 additions and 41 deletions

View File

@ -665,6 +665,25 @@ FMT_CONSTEXPR void for_each_codepoint(string_view s, F f) {
} while (buf_ptr < buf + num_chars_left);
}
template <typename Char, typename F,
FMT_ENABLE_IF(!std::is_same<Char, char>::value)>
FMT_CONSTEXPR void for_each_codepoint(basic_string_view<Char> s, F f) {
auto p = s.begin(), end = s.end();
while (p != end) {
auto begin = p;
uint32_t cp = static_cast<unsigned_char<Char>>(*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<unsigned_char<Char>>(*p);
if (low >= 0xdc00 && low <= 0xdfff) {
cp = 0x10000 + ((cp - 0xd800) << 10) + (low - 0xdc00);
++p;
}
}
if (!f(cp, basic_string_view<Char>(begin, to_unsigned(p - begin)))) return;
}
}
struct wide_cp_range {
uint32_t first;
uint32_t last;
@ -1982,12 +2001,17 @@ template <typename Char> struct find_escape_result {
template <typename Char>
auto find_escape(const Char* begin, const Char* end)
-> find_escape_result<Char> {
for (; begin != end; ++begin) {
uint32_t cp = static_cast<unsigned_char<Char>>(*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<Char>{end, nullptr, 0};
for_each_codepoint(basic_string_view<Char>(begin, to_unsigned(end - begin)),
[&](uint32_t cp, basic_string_view<Char> 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<Char>(out, static_cast<unsigned_type>(value), specs, loc);
}
template <typename Char, typename OutputIt,
FMT_ENABLE_IF(std::is_same<Char, char>::value)>
template <typename Char, typename OutputIt>
FMT_CONSTEXPR auto write(OutputIt out, basic_string_view<Char> 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<char>(s, it);
return is_debug ? write_escaped_string(it, s) : copy<Char>(s, it);
}
size_t display_width_limit =
@ -2421,12 +2444,13 @@ FMT_CONSTEXPR auto write(OutputIt out, basic_string_view<Char> 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<char> buf;
write_escaped_cp(basic_appender<char>(buf),
find_escape_result<char>{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<Char> sv) {
if (is_debug && needs_escape(cp) &&
(sizeof(Char) != 1 || std::is_same<Char, char>::value || cp < 0x80)) {
counting_buffer<Char> buf;
write_escaped_cp(basic_appender<Char>(buf),
find_escape_result<Char>{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<Char> 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<Char> s,
}
};
return write_padded<char>(
return write_padded<Char>(
out, specs, size, display_width, [=](reserve_iterator<OutputIt> it) {
return is_debug
? write_escaped_string(bounded_output_iterator{it, size}, s)
.underlying_iterator
: copy<char>(s.data(), s.data() + size, it);
});
}
template <typename Char, typename OutputIt,
FMT_ENABLE_IF(!std::is_same<Char, char>::value)>
FMT_CONSTEXPR auto write(OutputIt out, basic_string_view<Char> 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<Char>();
write_escaped_string(basic_appender<Char>(buf), s);
size = buf.count();
}
return write_padded<Char>(
out, specs, size, [=](reserve_iterator<OutputIt> it) {
return is_debug ? write_escaped_string(it, s)
: copy<Char>(data, data + size, it);
: copy<Char>(s.data(), s.data() + size, it);
});
}

View File

@ -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<char16_t>(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<std::u16string>{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