diff --git a/crates/doc/src/attrs.rs b/crates/doc/src/attrs.rs index 7215c156..274a79f2 100644 --- a/crates/doc/src/attrs.rs +++ b/crates/doc/src/attrs.rs @@ -190,6 +190,13 @@ impl Digits { } } +/// Does `c` belong to a script that capitalizes the first letter of sentences, words and headings? +/// Georgian doesn't: Unicode maps Mkhedruli to Mtavruli capitals, but those are only for all-caps +/// text, never for a sentence's, a title's or an index heading's first letter. +pub fn capitalizes(c: char) -> bool { + !matches!(c, '\u{10A0}'..='\u{10FF}' | '\u{1C90}'..='\u{1CBF}' | '\u{2D00}'..='\u{2D2F}') +} + /// The BCP 47 tag of a language name as InDesign and IDML write it ("English: USA", "Chinese: /// Simplified", or a locale code such as "de_DE_2006"), for the shaper's localized forms (`locl`) /// and everything that depends on the language (quotes, hyphenation and spelling, line breaking, @@ -272,6 +279,7 @@ pub fn language_tag(language: &str) -> Option<&'static str> { "persian" | "farsi" => "fa", "urdu" => "ur", "hebrew" => "he", + "georgian" => "ka", _ => return None, }) } @@ -1004,6 +1012,7 @@ mod tests { assert_eq!(language_tag("English: USA"), Some("en-US")); assert_eq!(language_tag("English: Australian"), Some("en")); assert_eq!(language_tag("Turkish"), Some("tr")); + assert_eq!(language_tag("Georgian"), Some("ka")); assert_eq!(language_tag("Japanese"), Some("ja")); assert_eq!(language_tag("Korean"), Some("ko")); assert_eq!(language_tag("Chinese"), Some("zh")); diff --git a/crates/doc/src/index.rs b/crates/doc/src/index.rs index 0c2e58f4..c885b7da 100644 --- a/crates/doc/src/index.rs +++ b/crates/doc/src/index.rs @@ -137,7 +137,12 @@ pub fn build(spec: &IndexSpec, refs: &[Resolved], page_name: &dyn Fn(usize) -> S let mut heading: Option = None; for (key, n) in &root { if spec.section_headings { - let c = key.chars().next().map(|c| if c.is_alphabetic() { c.to_uppercase().next().unwrap_or(c) } else { '#' }); + // Georgian headings stay Mkhedruli (no capitals; Mtavruli is only for all-caps text). + let c = key.chars().next().map(|c| match c { + c if !c.is_alphabetic() => '#', + c if crate::capitalizes(c) => c.to_uppercase().next().unwrap_or(c), + c => c, + }); if c != heading { heading = c; if let Some(c) = c { @@ -243,4 +248,12 @@ mod tests { let run_in = build(&IndexSpec { run_in: true, section_headings: false, title: String::new(), ..spec }, &refs[..2], &|p| (p + 1).to_string()); assert_eq!(run_in, [("Index Level 1".to_string(), "Typography 2\u{2013}4: kerning 5".to_string())]); } + #[test] + fn georgian_section_heads_stay_mkhedruli() { + let refs = vec![r(&["ასო"], IndexRange::CurrentPage, Some((0, 0))), r(&["ბადე"], IndexRange::CurrentPage, Some((1, 1)))]; + let out = build(&IndexSpec::default(), &refs, &|p| (p + 1).to_string()); + let heads: Vec<&str> = out.iter().filter(|(s, _)| s == "Index Section Head").map(|(_, t)| t.as_str()).collect(); + assert_eq!(heads, ["ა", "ბ"]); + assert!(crate::capitalizes('a') && !crate::capitalizes('ა') && !crate::capitalizes('Ა')); + } } diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index e3c98540..01263478 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -649,6 +649,22 @@ mod text_clipboard_tests { assert_eq!(s.doc().unwrap().selection.text.unwrap().range(), 0..st.len()); st.check().unwrap(); } + + #[test] + fn change_case_leaves_georgian_without_initial_capitals() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let a = frame(&mut s, "ᲒᲐᲛᲐᲠᲯᲝᲑᲐ ᲛᲡᲝᲤᲚᲘᲝ. ᲐᲮᲐᲚᲘ word"); + let len = s.doc().unwrap().doc.story(a).unwrap().len(); + s.execute("text.select", &json!({"story": a.0, "anchor": 0, "focus": len})).unwrap(); + // Georgian has no title or sentence capitals: Mtavruli is only for all-caps text. + s.execute("type.changeCase", &json!({"case": "title"})).unwrap(); + assert_eq!(s.doc().unwrap().doc.story(a).unwrap().text, "გამარჯობა მსოფლიო. ახალი Word"); + s.execute("type.changeCase", &json!({"case": "sentence"})).unwrap(); + assert_eq!(s.doc().unwrap().doc.story(a).unwrap().text, "გამარჯობა მსოფლიო. ახალი word"); + s.execute("type.changeCase", &json!({"case": "upper"})).unwrap(); + assert_eq!(s.doc().unwrap().doc.story(a).unwrap().text, "ᲒᲐᲛᲐᲠᲯᲝᲑᲐ ᲛᲡᲝᲤᲚᲘᲝ. ᲐᲮᲐᲚᲘ WORD"); + } } fn paste_into(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index add94aca..d7e8b0c8 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -1436,19 +1436,21 @@ pub(crate) fn case_map(src: &str, case: &str) -> Vec { let word_start = !(prev.is_alphanumeric() || matches!(prev, '\'' | '\u{2019}')); let up = |c: char| c.to_uppercase().collect::(); let low = |c: char| c.to_lowercase().collect::(); + // Title and sentence case capitalize only scripts with initial capitals (not Georgian). + let cap = |c: char| if designcraft_doc::capitalizes(c) { up(c) } else { low(c) }; let m = match case { "upper" => up(c), "lower" => low(c), "title" => { if word_start { - up(c) + cap(c) } else { low(c) } } _ => { if sentence_start && c.is_alphanumeric() { - up(c) + cap(c) } else { low(c) } diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 358ec76d..c2d17370 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -1257,6 +1257,7 @@ pub(crate) const LANGUAGES: &[&str] = &[ "Greek", "Russian", "Ukrainian", + "Georgian", "Turkish", "Japanese", "Chinese",