1use citum_schema::NoteStartTextCase;
13use citum_schema::options::titles::TextCase;
14use icu_casemap::CaseMapper;
15use icu_locale::LanguageIdentifier;
16
17#[must_use]
25pub fn apply_text_case(text: &str, case: TextCase) -> String {
26 apply_text_case_with_language(text, case, None)
27}
28
29#[must_use]
34pub fn apply_text_case_with_language(text: &str, case: TextCase, language: Option<&str>) -> String {
35 let language = language_identifier_for_tag(language);
36 apply_text_case_with_language_id(text, case, &language)
37}
38
39pub(crate) fn apply_text_case_with_language_id(
40 text: &str,
41 case: TextCase,
42 language: &LanguageIdentifier,
43) -> String {
44 match case {
45 TextCase::AsIs => text.to_string(),
46 TextCase::Lowercase => lowercase(text, language),
47 TextCase::Uppercase => uppercase(text, language),
48 TextCase::CapitalizeFirst => capitalize_first_word_with_language_id(text, language),
49 TextCase::Sentence | TextCase::SentenceApa | TextCase::SentenceNlm => {
50 to_sentence_case_with_language_id(text, language)
51 }
52 TextCase::Title => to_title_case_with_language_id(text, language),
53 }
54}
55
56pub(crate) fn language_identifier_for_tag(language: Option<&str>) -> LanguageIdentifier {
57 super::parse_language_identifier(language).unwrap_or_else(root_language_identifier)
58}
59
60fn root_language_identifier() -> LanguageIdentifier {
61 icu_locale::langid!("und")
62}
63
64fn lowercase(text: &str, language: &LanguageIdentifier) -> String {
65 CaseMapper::new()
66 .lowercase_to_string(text, language)
67 .into_owned()
68}
69
70fn uppercase(text: &str, language: &LanguageIdentifier) -> String {
71 CaseMapper::new()
72 .uppercase_to_string(text, language)
73 .into_owned()
74}
75
76#[must_use]
83pub fn apply_to_structured_parts(
84 main: &str,
85 subtitles: &[&str],
86 case: TextCase,
87) -> (String, Vec<String>) {
88 apply_to_structured_parts_with_language(main, subtitles, case, None)
89}
90
91#[must_use]
93pub fn apply_to_structured_parts_with_language(
94 main: &str,
95 subtitles: &[&str],
96 case: TextCase,
97 language: Option<&str>,
98) -> (String, Vec<String>) {
99 let language = language_identifier_for_tag(language);
100 match case {
101 TextCase::SentenceApa => {
102 let main_cased = to_sentence_case_with_language_id(main, &language);
103 let subs_cased = subtitles
104 .iter()
105 .map(|s| to_sentence_case_with_language_id(s, &language))
106 .collect();
107 (main_cased, subs_cased)
108 }
109 TextCase::SentenceNlm => {
110 let main_cased = to_sentence_case_with_language_id(main, &language);
111 let subs_cased = subtitles.iter().map(|s| lowercase(s, &language)).collect();
113 (main_cased, subs_cased)
114 }
115 _ => {
116 let main_cased = apply_text_case_with_language_id(main, case, &language);
117 let subs_cased = subtitles
118 .iter()
119 .map(|s| apply_text_case_with_language_id(s, case, &language))
120 .collect();
121 (main_cased, subs_cased)
122 }
123 }
124}
125
126#[must_use]
128pub fn is_english_language(lang: Option<&str>) -> bool {
129 match lang {
130 Some(tag) => {
131 let primary = tag.split('-').next().unwrap_or(tag);
132 primary.eq_ignore_ascii_case("en")
133 }
134 None => true,
136 }
137}
138
139#[must_use]
143pub fn resolve_text_case(case: TextCase, language: Option<&str>) -> TextCase {
144 if is_english_language(language) {
145 case
146 } else {
147 match case {
150 TextCase::AsIs | TextCase::Lowercase | TextCase::Uppercase => case,
151 _ => TextCase::AsIs,
152 }
153 }
154}
155
156#[must_use]
159pub(crate) fn apply_note_start_text_case(
160 value: &str,
161 text_case: NoteStartTextCase,
162 language: Option<&str>,
163) -> String {
164 let case = match text_case {
165 NoteStartTextCase::CapitalizeFirst => TextCase::CapitalizeFirst,
166 NoteStartTextCase::Lowercase => TextCase::Lowercase,
167 };
168 apply_text_case_with_language(value, resolve_text_case(case, language), language)
169}
170
171fn has_internal_uppercase(word: &str) -> bool {
180 let mut chars = word.chars();
181 chars.next();
182 chars.any(char::is_uppercase)
183}
184
185fn rebuild_with_original_whitespace(text: &str, parts: &[String]) -> String {
189 let mut result = String::with_capacity(text.len());
190 let mut word_iter = parts.iter();
191 let mut in_word = false;
192 let mut current_word = word_iter.next();
193
194 for ch in text.chars() {
195 if ch.is_whitespace() {
196 if in_word {
197 in_word = false;
198 current_word = word_iter.next();
199 }
200 result.push(ch);
201 } else if !in_word && let Some(word) = current_word {
202 result.push_str(word);
203 in_word = true;
204 }
205 }
206
207 result
208}
209
210#[cfg(test)]
215fn to_sentence_case(text: &str) -> String {
216 let language = root_language_identifier();
217 to_sentence_case_with_language_id(text, &language)
218}
219
220fn to_sentence_case_with_language_id(text: &str, language: &LanguageIdentifier) -> String {
221 if text.is_empty() {
222 return String::new();
223 }
224
225 let words: Vec<&str> = text.split_whitespace().collect();
226 if words.is_empty() {
227 return text.to_string();
228 }
229
230 let mut parts: Vec<String> = Vec::with_capacity(words.len());
231 for (i, word) in words.iter().enumerate() {
232 if has_internal_uppercase(word) {
233 parts.push((*word).to_string());
234 } else if i == 0 {
235 parts.push(capitalize_first_word_with_language_id(
236 &lowercase(word, language),
237 language,
238 ));
239 } else {
240 parts.push(lowercase(word, language));
241 }
242 }
243
244 rebuild_with_original_whitespace(text, &parts)
245}
246
247#[cfg(test)]
256pub(crate) fn capitalize_first_word(text: &str) -> String {
257 let language = root_language_identifier();
258 capitalize_first_word_with_language_id(text, &language)
259}
260
261fn capitalize_first_word_with_language_id(text: &str, language: &LanguageIdentifier) -> String {
262 let mut result = String::with_capacity(text.len());
263 let mut found_first = false;
264 let mut blocked_by_digit = false;
265 for ch in text.chars() {
266 if !found_first && !blocked_by_digit && ch.is_alphabetic() {
267 result.push_str(&uppercase(&ch.to_string(), language));
268 found_first = true;
269 } else {
270 if !found_first && ch.is_ascii_digit() {
271 blocked_by_digit = true;
272 }
273 result.push(ch);
274 }
275 }
276 result
277}
278
279#[cfg(test)]
286pub(crate) fn capitalize_first_word_markup_aware(text: &str) -> String {
287 capitalize_first_word_markup_aware_with_language(text, None)
288}
289
290pub(crate) fn capitalize_first_word_markup_aware_with_language(
291 text: &str,
292 language: Option<&str>,
293) -> String {
294 let language = language_identifier_for_tag(language);
295 let bytes = text.as_bytes();
296 let len = bytes.len();
297 let mut i = 0;
298
299 while i < len {
300 let Some(&b) = bytes.get(i) else { break };
301
302 if b == b'<'
305 && let Some(end) = text.get(i..).and_then(|s| s.find('>'))
306 {
307 i += end + 1;
308 continue;
309 }
310
311 if b == b'\\' {
313 let cmd_start = i + 1;
314 let cmd_len = bytes
315 .get(cmd_start..)
316 .unwrap_or_default()
317 .iter()
318 .take_while(|&&c| c.is_ascii_alphabetic())
319 .count();
320 if cmd_len > 0 {
321 let after_cmd = cmd_start + cmd_len;
322 let after_opt = if bytes.get(after_cmd) == Some(&b'[') {
324 text.get(after_cmd..)
325 .and_then(|s| s.find(']'))
326 .map(|e| after_cmd + e + 1)
327 .unwrap_or(after_cmd)
328 } else {
329 after_cmd
330 };
331 if bytes.get(after_opt) == Some(&b'{') {
332 i = after_opt + 1;
333 continue;
334 }
335 }
336 }
337
338 if b == b'#' {
340 let cmd_start = i + 1;
341 let cmd_len = bytes
342 .get(cmd_start..)
343 .unwrap_or_default()
344 .iter()
345 .take_while(|&&c| c.is_ascii_alphabetic())
346 .count();
347 if cmd_len > 0 {
348 let after_cmd = cmd_start + cmd_len;
349 if bytes.get(after_cmd) == Some(&b'[') {
350 i = after_cmd + 1;
351 continue;
352 }
353 }
354 }
355
356 let ch = text.get(i..).and_then(|s| s.chars().next()).unwrap_or('\0');
359 if ch.is_alphabetic() {
360 let ch_len = ch.len_utf8();
361 let mut result = String::with_capacity(text.len());
362 result.push_str(text.get(..i).unwrap_or_default());
363 result.push_str(&uppercase(&ch.to_string(), &language));
364 result.push_str(text.get(i + ch_len..).unwrap_or_default());
365 return result;
366 }
367
368 i += ch.len_utf8().max(1);
369 }
370
371 text.to_string()
372}
373
374pub(crate) fn apply_text_case_markup_aware_with_language(
380 text: &str,
381 case: TextCase,
382 language: Option<&str>,
383) -> String {
384 match case {
385 TextCase::CapitalizeFirst => {
386 capitalize_first_word_markup_aware_with_language(text, language)
387 }
388 _ => apply_text_case_with_language(text, case, language),
389 }
390}
391
392const TITLE_CASE_STOP_WORDS: &[&str] = &[
394 "a", "an", "and", "as", "at", "but", "by", "for", "from", "in", "nor", "of", "on", "or", "so",
395 "the", "to", "up", "yet", "v", "vs",
396];
397
398const HYPHEN_LIKE_CHARS: [char; 2] = ['-', '\u{2013}'];
405
406fn contains_hyphen_like(text: &str) -> bool {
407 text.contains(HYPHEN_LIKE_CHARS)
408}
409
410fn capitalize_hyphenated(word: &str, force_all: bool, language: &LanguageIdentifier) -> String {
417 let mut result = String::with_capacity(word.len());
418 let mut last_end = 0;
419 for (idx, sep) in word.match_indices(HYPHEN_LIKE_CHARS) {
420 let part = word.get(last_end..idx).unwrap_or_default();
421 result.push_str(&capitalize_hyphen_part(part, force_all, language));
422 result.push_str(sep);
423 last_end = idx + sep.len();
424 }
425 let tail = word.get(last_end..).unwrap_or_default();
426 result.push_str(&capitalize_hyphen_part(tail, force_all, language));
427 result
428}
429
430fn capitalize_hyphen_part(part: &str, force_all: bool, language: &LanguageIdentifier) -> String {
431 if force_all {
432 capitalize_first_word_with_language_id(part, language)
433 } else {
434 let alpha_core = part.trim_matches(|c: char| !c.is_alphanumeric());
435 if TITLE_CASE_STOP_WORDS.contains(&alpha_core) {
436 part.to_string()
437 } else {
438 capitalize_first_word_with_language_id(part, language)
439 }
440 }
441}
442
443fn trim_trailing_closing_punctuation(word: &str) -> &str {
444 word.trim_end_matches(['"', '\'', ')', ']', '}', '»', '”', '’'])
445}
446
447#[cfg(test)]
454fn to_title_case(text: &str) -> String {
455 let language = root_language_identifier();
456 to_title_case_with_language_id(text, &language)
457}
458
459fn to_title_case_with_language_id(text: &str, language: &LanguageIdentifier) -> String {
460 if text.is_empty() {
461 return String::new();
462 }
463
464 let words: Vec<&str> = text.split_whitespace().collect();
465 if words.is_empty() {
466 return text.to_string();
467 }
468
469 let last_idx = words.len() - 1;
470 let mut parts: Vec<String> = Vec::with_capacity(words.len());
471 let mut capitalize_next = false;
472
473 for (i, word) in words.iter().enumerate() {
474 if has_internal_uppercase(word) {
475 parts.push((*word).to_string());
478 } else {
479 let lower = lowercase(word, language);
480 if i == 0 || i == last_idx || capitalize_next {
481 if contains_hyphen_like(&lower) {
482 parts.push(capitalize_hyphenated(&lower, true, language));
483 } else {
484 parts.push(capitalize_first_word_with_language_id(&lower, language));
485 }
486 } else {
487 let alpha_core = lower.trim_matches(|c: char| !c.is_alphanumeric());
490 if TITLE_CASE_STOP_WORDS.contains(&alpha_core) {
491 parts.push(lower);
492 } else if contains_hyphen_like(&lower) {
493 parts.push(capitalize_hyphenated(&lower, false, language));
494 } else {
495 parts.push(capitalize_first_word_with_language_id(&lower, language));
496 }
497 }
498 }
499 let punctuation_core = trim_trailing_closing_punctuation(word);
502 capitalize_next = punctuation_core.ends_with(':')
503 || punctuation_core.ends_with('?')
504 || punctuation_core.ends_with('!');
505 }
506
507 rebuild_with_original_whitespace(text, &parts)
508}
509
510#[cfg(test)]
511#[allow(
512 clippy::unwrap_used,
513 clippy::expect_used,
514 clippy::panic,
515 clippy::indexing_slicing,
516 clippy::todo,
517 clippy::unimplemented,
518 clippy::unreachable,
519 clippy::get_unwrap,
520 reason = "Panicking is acceptable and often desired in tests."
521)]
522mod tests {
523 use super::*;
524
525 #[test]
528 fn test_capitalize_first_word_basic() {
529 assert_eq!(capitalize_first_word("hello world"), "Hello world");
530 }
531
532 #[test]
533 fn test_capitalize_first_word_leading_space() {
534 assert_eq!(capitalize_first_word(" hello"), " Hello");
535 }
536
537 #[test]
538 fn test_capitalize_first_word_empty() {
539 assert_eq!(capitalize_first_word(""), "");
540 }
541
542 #[test]
543 fn test_capitalize_first_word_already_upper() {
544 assert_eq!(capitalize_first_word("Hello"), "Hello");
545 }
546
547 #[test]
548 fn given_leading_numeral_when_capitalize_first_word_then_left_as_is() {
549 assert_eq!(capitalize_first_word("35 mm film"), "35 mm film");
552 }
553
554 #[test]
555 fn given_leading_letter_when_capitalize_first_word_then_capitalized() {
556 assert_eq!(capitalize_first_word("in Korean"), "In Korean");
557 }
558
559 #[test]
560 fn turkish_case_mapping_handles_dotted_and_dotless_i() {
561 assert_eq!(
562 apply_text_case_with_language("istanbul ızmir", TextCase::Uppercase, Some("tr-TR"),),
563 "İSTANBUL IZMİR"
564 );
565 assert_eq!(
566 apply_text_case_with_language("İSTANBUL IĞDIR", TextCase::Lowercase, Some("tr-TR"),),
567 "istanbul ığdır"
568 );
569 assert_eq!(
570 apply_text_case_with_language("istanbul", TextCase::CapitalizeFirst, Some("tr")),
571 "İstanbul"
572 );
573 assert_eq!(
574 apply_text_case_with_language("istanbul ızmir", TextCase::Uppercase, Some("tr_TR")),
575 "İSTANBUL IZMİR"
576 );
577 }
578
579 #[test]
580 fn azerbaijani_case_mapping_uses_turkic_rules() {
581 assert_eq!(
582 apply_text_case_with_language("izmir ı", TextCase::Uppercase, Some("az-Latn-AZ")),
583 "İZMİR I"
584 );
585 }
586
587 #[test]
588 fn sentence_case_uses_the_supplied_language() {
589 assert_eq!(
590 apply_text_case_with_language("istanbul Izmir", TextCase::Sentence, Some("tr-TR"),),
591 "İstanbul ızmir"
592 );
593 }
594
595 #[test]
596 fn structured_subtitles_use_the_supplied_language() {
597 let (main, subtitles) = apply_to_structured_parts_with_language(
598 "istanbul tarihi",
599 &["izmir incelemesi"],
600 TextCase::SentenceApa,
601 Some("tr-TR"),
602 );
603
604 assert_eq!(main, "İstanbul tarihi");
605 assert_eq!(subtitles, ["İzmir incelemesi"]);
606 }
607
608 #[test]
609 fn malformed_language_tag_uses_root_case_mapping() {
610 assert_eq!(
611 apply_text_case_with_language("istanbul", TextCase::Uppercase, Some("not_a_language"),),
612 "ISTANBUL"
613 );
614 }
615
616 #[test]
617 fn locale_override_suffix_preserves_base_language_mapping() {
618 assert_eq!(
619 apply_text_case_with_language(
620 "istanbul",
621 TextCase::Uppercase,
622 Some("tr-TR-citum_override"),
623 ),
624 "İSTANBUL"
625 );
626 }
627
628 #[test]
631 fn test_capitalize_markup_aware_plain_text() {
632 assert_eq!(
633 capitalize_first_word_markup_aware("the collected essays"),
634 "The collected essays"
635 );
636 }
637
638 #[test]
639 fn test_capitalize_markup_aware_html_tag() {
640 assert_eq!(
641 capitalize_first_word_markup_aware("<em>the collected essays</em>"),
642 "<em>The collected essays</em>"
643 );
644 }
645
646 #[test]
647 fn capitalize_markup_aware_uses_the_supplied_language() {
648 assert_eq!(
649 apply_text_case_markup_aware_with_language(
650 "<em>istanbul</em>",
651 TextCase::CapitalizeFirst,
652 Some("tr-TR"),
653 ),
654 "<em>İstanbul</em>"
655 );
656 }
657
658 #[test]
659 fn test_capitalize_markup_aware_html_nested_tags() {
660 assert_eq!(
661 capitalize_first_word_markup_aware(r#"<span class="x"><em>the title</em></span>"#),
662 r#"<span class="x"><em>The title</em></span>"#
663 );
664 }
665
666 #[test]
667 fn test_capitalize_markup_aware_latex_command() {
668 assert_eq!(
669 capitalize_first_word_markup_aware(r"\emph{the collected essays}"),
670 r"\emph{The collected essays}"
671 );
672 }
673
674 #[test]
675 fn test_capitalize_markup_aware_latex_number_not_corrupted() {
676 assert_eq!(
678 capitalize_first_word_markup_aware(r"\emph{521}"),
679 r"\emph{521}"
680 );
681 }
682
683 #[test]
684 fn test_capitalize_markup_aware_typst_command() {
685 assert_eq!(
686 capitalize_first_word_markup_aware("#emph[the collected essays]"),
687 "#emph[The collected essays]"
688 );
689 }
690
691 #[test]
692 fn test_capitalize_markup_aware_plain_underscore_delimiters() {
693 assert_eq!(
695 capitalize_first_word_markup_aware("_the collected essays_"),
696 "_The collected essays_"
697 );
698 }
699
700 #[test]
701 fn test_capitalize_markup_aware_empty_string() {
702 assert_eq!(capitalize_first_word_markup_aware(""), "");
703 }
704
705 #[test]
706 fn test_capitalize_markup_aware_all_markup_no_text() {
707 assert_eq!(capitalize_first_word_markup_aware("<em></em>"), "<em></em>");
708 }
709
710 #[test]
713 fn test_sentence_case_basic() {
714 assert_eq!(
715 to_sentence_case("The Quick Brown Fox"),
716 "The quick brown fox"
717 );
718 }
719
720 #[test]
721 fn test_sentence_case_all_caps() {
722 assert_eq!(to_sentence_case("DNA REPLICATION"), "DNA REPLICATION");
725 }
726
727 #[test]
728 fn test_sentence_case_empty() {
729 assert_eq!(to_sentence_case(""), "");
730 }
731
732 #[test]
733 fn given_mixed_case_word_when_sentence_case_then_preserved() {
734 assert_eq!(
735 to_sentence_case("An Introduction to DNA"),
736 "An introduction to DNA"
737 );
738 }
739
740 #[test]
741 fn given_lowercase_iphone_when_sentence_case_then_lowercased() {
742 assert_eq!(to_sentence_case("the iphone problem"), "The iphone problem");
743 }
744
745 #[test]
746 fn given_mixed_case_iphone_when_sentence_case_then_preserved() {
747 assert_eq!(to_sentence_case("the iPhone problem"), "The iPhone problem");
748 }
749
750 #[test]
751 fn given_mixed_case_surname_when_sentence_case_then_preserved() {
752 assert_eq!(
753 to_sentence_case("a study of McDonald"),
754 "A study of McDonald"
755 );
756 }
757
758 #[test]
761 fn test_title_case_basic() {
762 assert_eq!(to_title_case("the quick brown fox"), "The Quick Brown Fox");
763 }
764
765 #[test]
766 fn test_title_case_stop_words() {
767 assert_eq!(
768 to_title_case("a tale of two cities"),
769 "A Tale of Two Cities"
770 );
771 }
772
773 #[test]
774 fn test_title_case_last_word_capitalized() {
775 assert_eq!(
776 to_title_case("the world we live in"),
777 "The World We Live In"
778 );
779 }
780
781 #[test]
782 fn test_title_case_after_colon() {
783 assert_eq!(
784 to_title_case("the title: a subtitle"),
785 "The Title: A Subtitle"
786 );
787 }
788
789 #[test]
790 fn test_title_case_after_colon_stop_word() {
791 assert_eq!(
793 to_title_case("history of the world: a new perspective"),
794 "History of the World: A New Perspective"
795 );
796 }
797
798 #[test]
799 fn test_title_case_after_question_mark() {
800 assert_eq!(
801 to_title_case("who's black and why? a hidden chapter"),
802 "Who's Black and Why? A Hidden Chapter"
803 );
804 }
805
806 #[test]
807 fn test_title_case_after_question_mark_with_closing_quote() {
808 assert_eq!(
809 to_title_case("who's black and why?\" a hidden chapter"),
810 "Who's Black and Why?\" A Hidden Chapter"
811 );
812 }
813
814 #[test]
815 fn test_title_case_from_is_stop_word() {
816 assert_eq!(
817 to_title_case("a hidden chapter from the eighteenth-century invention of race"),
818 "A Hidden Chapter from the Eighteenth-Century Invention of Race"
819 );
820 }
821
822 #[test]
823 fn test_title_case_hyphenated_compound() {
824 assert_eq!(
825 to_title_case("eighteenth-century studies"),
826 "Eighteenth-Century Studies"
827 );
828 }
829
830 #[test]
831 fn test_title_case_hyphenated_stop_word_part() {
832 assert_eq!(to_title_case("a well-to-do family"), "A Well-to-Do Family");
834 }
835
836 #[test]
837 fn given_en_dash_compound_when_title_case_then_both_sides_capitalized() {
838 assert_eq!(
842 to_title_case("the aging\u{2013}disability nexus"),
843 "The Aging\u{2013}Disability Nexus"
844 );
845 }
846
847 #[test]
850 fn test_sentence_apa_structured() {
851 let (main, subs) = apply_to_structured_parts(
852 "Understanding Citation Systems",
853 &["History and Practice", "A Comparative View"],
854 TextCase::SentenceApa,
855 );
856 assert_eq!(main, "Understanding citation systems");
857 assert_eq!(subs, vec!["History and practice", "A comparative view"]);
858 }
859
860 #[test]
861 fn test_sentence_nlm_structured() {
862 let (main, subs) = apply_to_structured_parts(
863 "Understanding Citation Systems",
864 &["History and Practice"],
865 TextCase::SentenceNlm,
866 );
867 assert_eq!(main, "Understanding citation systems");
868 assert_eq!(subs, vec!["history and practice"]);
870 }
871
872 #[test]
873 fn test_title_case_structured() {
874 let (main, subs) =
877 apply_to_structured_parts("the DNA of empire", &["a new perspective"], TextCase::Title);
878 assert_eq!(main, "The DNA of Empire");
879 assert_eq!(subs, vec!["A New Perspective"]);
880 }
881
882 #[test]
883 fn given_mixed_case_surname_when_title_case_then_preserved() {
884 assert_eq!(to_title_case("a study of McDonald"), "A Study of McDonald");
885 }
886
887 #[test]
890 fn test_english_language_detection() {
891 assert!(is_english_language(Some("en")));
892 assert!(is_english_language(Some("en-US")));
893 assert!(is_english_language(Some("en-GB")));
894 assert!(is_english_language(None));
895 assert!(!is_english_language(Some("de")));
896 assert!(!is_english_language(Some("fr-FR")));
897 }
898
899 #[test]
900 fn test_resolve_non_english_falls_back() {
901 assert_eq!(
902 resolve_text_case(TextCase::SentenceApa, Some("de")),
903 TextCase::AsIs
904 );
905 assert_eq!(
906 resolve_text_case(TextCase::Title, Some("fr")),
907 TextCase::AsIs
908 );
909 assert_eq!(
911 resolve_text_case(TextCase::Lowercase, Some("de")),
912 TextCase::Lowercase
913 );
914 }
915
916 #[test]
917 fn test_resolve_english_passes_through() {
918 assert_eq!(
919 resolve_text_case(TextCase::SentenceApa, Some("en")),
920 TextCase::SentenceApa
921 );
922 assert_eq!(
923 resolve_text_case(TextCase::Title, Some("en-US")),
924 TextCase::Title
925 );
926 }
927
928 #[test]
929 fn test_note_start_capitalize_first_uses_english_language_rules() {
930 assert_eq!(
931 apply_note_start_text_case(
932 "edited by",
933 NoteStartTextCase::CapitalizeFirst,
934 Some("en-US"),
935 ),
936 "Edited by"
937 );
938 }
939
940 #[test]
941 fn test_note_start_capitalize_first_falls_back_to_as_is_for_non_english() {
942 assert_eq!(
943 apply_note_start_text_case(
944 "hg. von",
945 NoteStartTextCase::CapitalizeFirst,
946 Some("de-DE"),
947 ),
948 "hg. von"
949 );
950 }
951
952 #[test]
953 fn test_note_start_capitalize_first_is_no_op_for_uncased_scripts() {
954 assert_eq!(
955 apply_note_start_text_case("ابن سينا", NoteStartTextCase::CapitalizeFirst, Some("ar"),),
956 "ابن سينا"
957 );
958 }
959}