Titlecase the first-of-word code point without leading adjustment (#8241)

case::{title,capitalize}_wtf8 pass a single first-of-word code point to
TitlecaseMapper::titlecase_segment. Under the default Auto leading
adjustment the mapper looks for a Letter/Number/Symbol/Private_Use head
and skips anything else, dropping the titlecase mapping of cased marks
such as U+0345 (COMBINING GREEK YPOGEGRAMMENI) -> U+0399: 'ͅ'.title()
returned 'ͅ' instead of 'Ι'. Use LeadingAdjustment::None so the
given code point is titlecased directly, matching CPython str.title and
str.capitalize.

Assisted-by: Claude
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-07-09 01:38:30 +09:00
1 parent 797d931f7a
commit c03e266c9b
1 file changed
+27 -1
+27 -1
View File
@@ -19,6 +19,7 @@ use alloc::{
vec::Vec,
};
use icu_casemap::options::{LeadingAdjustment, TitlecaseOptions};
use icu_casemap::{CaseMapper, TitlecaseMapper};
use icu_locale::LanguageIdentifier;
use icu_properties::props::{
@@ -236,8 +237,16 @@ fn titlecase_string(s: &str, out: &mut FmtWriter<'_>) {
}
fn titlecase_segment(s: &str, out: &mut FmtWriter<'_>) {
// Callers pass a single first-of-word code point, which Python titlecases
// unconditionally (applying its titlecase mapping). The default `Auto`
// leading adjustment looks for a head in Letter/Number/Symbol/Private_Use
// and skips anything else, dropping the titlecase mapping of cased marks
// such as U+0345 (`ͅ`, general category Mn) -> U+0399 (`Ι`). `None`
// titlecases the code point as given.
let mut options = TitlecaseOptions::default();
options.leading_adjustment = Some(LeadingAdjustment::None);
TitlecaseMapper::new()
.titlecase_segment(s, &LanguageIdentifier::UNKNOWN, Default::default())
.titlecase_segment(s, &LanguageIdentifier::UNKNOWN, options)
.write_to(out)
.expect("writing to an in-memory buffer cannot fail");
}
@@ -353,6 +362,23 @@ mod tests {
assert_eq!(swapcase_str("Hello"), "hELLO");
}
#[test]
fn titlecase_first_of_word_takes_titlecase_mapping() {
// A leading cased combining mark still takes its titlecase mapping:
// U+0345 (ͅ, general category Mn) titlecases to U+0399 (Ι), even though
// it is not a Letter/Number/Symbol head.
assert_eq!(title_str("\u{0345}"), "\u{0399}");
assert_eq!(capitalize_str("\u{0345}"), "\u{0399}");
assert_eq!(title_str("\u{0345}a"), "\u{0399}a");
// Full (one-to-many) titlecase mappings still apply to the first
// character of each word.
// cspell:ignore finnish NNISH dzungla Dzungla ßhello Sshello
assert_eq!(capitalize_str("finnish"), "Finnish");
assert_eq!(title_str("fiNNISH"), "Finnish");
assert_eq!(capitalize_str("dzungla"), "Dzungla");
assert_eq!(capitalize_str("ßhello"), "Sshello");
}
#[test]
fn wtf8_passes_surrogates_through() {
let mut buf = Wtf8Buf::from("ab cd");