mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 02:17:24 -06:00
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ use wordcraft_doc::resolve::ResolvedChar;
|
||||
use wordcraft_doc::{InlineObject, PartKind, Pos, RevisionKind, StoryRef};
|
||||
|
||||
use super::sel_result;
|
||||
use super::tools::{is_turkic, lower_char, upper_char};
|
||||
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
|
||||
|
||||
/// Word's font size list.
|
||||
@@ -453,7 +454,16 @@ fn change_case(s: &mut Session, v: &Value) -> CmdResult {
|
||||
let to = if path == b.path { b.off } else { p.len() };
|
||||
let chars = live_chars(p, from, to);
|
||||
let src: String = chars.iter().map(|(_, c)| c).collect();
|
||||
case_groups(p, &chars, &convert_chars(&src, &mode, from == 0))
|
||||
// Turkish, Azerbaijani and Crimean Tatar text follows the same case mapping as
|
||||
// AutoCorrect (#1923); each run's own language decides (#3479).
|
||||
let turkic: Vec<bool> = chars
|
||||
.iter()
|
||||
.map(|(off, _)| {
|
||||
let lang = s.doc.styles.resolve_char(p.props.style.as_deref(), p.props_of_char(*off)).lang;
|
||||
is_turkic(lang.as_deref().or(s.editing_lang.as_deref()))
|
||||
})
|
||||
.collect();
|
||||
case_groups(p, &chars, &convert_chars(&src, &mode, from == 0, &turkic))
|
||||
};
|
||||
if groups.is_empty() {
|
||||
continue;
|
||||
@@ -587,37 +597,47 @@ fn is_title(t: &str) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
fn convert(src: &str, mode: &str, para_start: bool) -> String {
|
||||
convert_chars(src, mode, para_start).concat()
|
||||
let turkic = vec![false; src.chars().count()];
|
||||
convert_chars(src, mode, para_start, &turkic).concat()
|
||||
}
|
||||
|
||||
/// The case-converted form of each character of `src`, in order (one entry per char, so a
|
||||
/// change of length such as ß → SS or İ → i̇ can be applied run by run).
|
||||
fn convert_chars(src: &str, mode: &str, para_start: bool) -> Vec<String> {
|
||||
/// change of length such as ß → SS or İ → i̇ can be applied run by run). `turkic` is one flag
|
||||
/// per char: true when that character's run is Turkish, Azerbaijani or Crimean Tatar.
|
||||
fn convert_chars(src: &str, mode: &str, para_start: bool, turkic: &[bool]) -> Vec<String> {
|
||||
let chars: Vec<char> = src.chars().collect();
|
||||
// Greek capital sigma lowers to the final form at the end of a word (as `str::to_lowercase`).
|
||||
let lower = |i: usize, c: char| -> String {
|
||||
let prev = i.checked_sub(1).and_then(|j| chars.get(j)).is_some_and(|p| p.is_alphabetic());
|
||||
let next = chars.get(i + 1).is_some_and(|n| n.is_alphabetic());
|
||||
if c == 'Σ' && prev && !next { "ς".to_string() } else { c.to_lowercase().collect() }
|
||||
if c == 'Σ' && prev && !next {
|
||||
"ς".to_string()
|
||||
} else if turkic.get(i).copied().unwrap_or(false) {
|
||||
lower_char(c, true)
|
||||
} else {
|
||||
c.to_lowercase().collect()
|
||||
}
|
||||
};
|
||||
// Title case capitalises the first word of any selection; sentence case only at a paragraph start.
|
||||
let mut up = mode == "title" || para_start;
|
||||
let mut out = Vec::with_capacity(chars.len());
|
||||
for (i, &c) in chars.iter().enumerate() {
|
||||
let turkic = turkic.get(i).copied().unwrap_or(false);
|
||||
let upper = |c: char| upper_char(c, turkic);
|
||||
let new = match mode {
|
||||
"upper" => c.to_uppercase().collect(),
|
||||
"upper" => upper(c),
|
||||
"lower" => lower(i, c),
|
||||
"toggle" => {
|
||||
if c.is_uppercase() {
|
||||
lower(i, c)
|
||||
} else {
|
||||
c.to_uppercase().collect()
|
||||
upper(c)
|
||||
}
|
||||
}
|
||||
"title" => {
|
||||
let n = if up && c.is_alphabetic() {
|
||||
up = false;
|
||||
c.to_uppercase().collect()
|
||||
upper(c)
|
||||
} else {
|
||||
lower(i, c)
|
||||
};
|
||||
@@ -630,7 +650,7 @@ fn convert_chars(src: &str, mode: &str, para_start: bool) -> Vec<String> {
|
||||
// Sentence case.
|
||||
let n = if up && c.is_alphabetic() {
|
||||
up = false;
|
||||
c.to_uppercase().collect()
|
||||
upper(c)
|
||||
} else {
|
||||
lower(i, c)
|
||||
};
|
||||
|
||||
@@ -1028,7 +1028,7 @@ fn prev_token(before: &str) -> String {
|
||||
}
|
||||
|
||||
/// Turkish, Azerbaijani (and Crimean Tatar) map `i`/`İ` and `ı`/`I` as pairs (#1923).
|
||||
fn is_turkic(lang: Option<&str>) -> bool {
|
||||
pub(crate) fn is_turkic(lang: Option<&str>) -> bool {
|
||||
if wordcraft_proof::ProofLang::from_tag(lang) == wordcraft_proof::ProofLang::Tr {
|
||||
return true; // tr, tur, any region, same normalisation as proofing
|
||||
}
|
||||
@@ -1038,7 +1038,7 @@ fn is_turkic(lang: Option<&str>) -> bool {
|
||||
}
|
||||
|
||||
/// A letter in capitals: `i` → `İ` in Turkic languages, otherwise the Unicode default.
|
||||
fn upper_char(c: char, turkic: bool) -> String {
|
||||
pub(crate) fn upper_char(c: char, turkic: bool) -> String {
|
||||
if turkic && c == 'i' {
|
||||
return "\u{130}".to_string();
|
||||
}
|
||||
@@ -1047,7 +1047,7 @@ fn upper_char(c: char, turkic: bool) -> String {
|
||||
|
||||
/// A letter in lowercase: `I` → `ı` in Turkic languages. `İ` is always the single letter `i`,
|
||||
/// never `i` plus a combining dot.
|
||||
fn lower_char(c: char, turkic: bool) -> String {
|
||||
pub(crate) fn lower_char(c: char, turkic: bool) -> String {
|
||||
match c {
|
||||
'I' if turkic => "\u{131}".to_string(),
|
||||
'\u{130}' => "i".to_string(),
|
||||
|
||||
@@ -2075,6 +2075,46 @@ fn autocorrect_capitalisation_follows_turkic_case_mapping() {
|
||||
assert_eq!(s.doc.plain_text(StoryRef::Body), "İngilizce ", "no combining dot in any language");
|
||||
}
|
||||
|
||||
/// #3479: Format > Change Case follows the same Turkic case mapping as AutoCorrect, using
|
||||
/// each run's own language; other languages keep the Unicode default.
|
||||
#[test]
|
||||
fn change_case_follows_turkic_case_mapping() {
|
||||
for tag in ["tr-TR", "az-AZ"] {
|
||||
for (mode, input, want) in [
|
||||
("upper", "istanbul", "İSTANBUL"),
|
||||
("lower", "IŞIK", "ışık"),
|
||||
("title", "istanbul güzel", "İstanbul Güzel"),
|
||||
("sentence", "istanbul. izmir", "İstanbul. İzmir"),
|
||||
("toggle", "iI", "İı"),
|
||||
] {
|
||||
let mut s = s();
|
||||
run(&mut s, "review.language", json!({"lang": tag, "default": true}));
|
||||
run(&mut s, "text.insert", json!({"text": input, "raw": true}));
|
||||
run(&mut s, "select.all", json!({}));
|
||||
run(&mut s, "format.changeCase", json!({"mode": mode}));
|
||||
assert_eq!(s.doc.plain_text(StoryRef::Body), want, "{tag} {mode}");
|
||||
}
|
||||
}
|
||||
// Other languages keep the Unicode default.
|
||||
let mut s = self::s();
|
||||
run(&mut s, "text.insert", json!({"text": "istanbul", "raw": true}));
|
||||
run(&mut s, "select.all", json!({}));
|
||||
run(&mut s, "format.changeCase", json!({"mode": "title"}));
|
||||
assert_eq!(s.doc.plain_text(StoryRef::Body), "Istanbul", "en-US");
|
||||
// Mixed runs: each run's own language decides.
|
||||
let mut s = self::s();
|
||||
run(&mut s, "text.insert", json!({"text": "IŞIK IŞIK", "raw": true}));
|
||||
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 0)}));
|
||||
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 5), "extend": true}));
|
||||
run(&mut s, "review.language", json!({"lang": "tr-TR"}));
|
||||
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 6)}));
|
||||
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 11), "extend": true}));
|
||||
run(&mut s, "review.language", json!({"lang": "en-US"}));
|
||||
run(&mut s, "select.all", json!({}));
|
||||
run(&mut s, "format.changeCase", json!({"mode": "lower"}));
|
||||
assert_eq!(s.doc.plain_text(StoryRef::Body), "ışık işik", "mixed runs");
|
||||
}
|
||||
|
||||
// Spelling as you type (#1552): the word being typed gets no red squiggle until the caret leaves
|
||||
// it; F7 and the Spelling pane still report it. See docs/typing-parity.md. Paragraphs start with
|
||||
// a correct word so AutoCorrect's sentence capital doesn't change the misspelled one.
|
||||
|
||||
@@ -187,8 +187,9 @@ Applied when a word is finished by a space, punctuation **or Enter** (`enter_fin
|
||||
`İngilizce`, `ılık` → `Ilık`; observed in Word for Mac with Turkish proofing). Lowercasing `İ`
|
||||
gives the single letter `i` in every language, never `i` + combining dot. Other languages keep
|
||||
the Unicode default (`istanbul` → `Istanbul` in English)
|
||||
(`autocorrect_capitalisation_follows_turkic_case_mapping`). Format > Change Case is not yet
|
||||
Turkish-aware.
|
||||
(`autocorrect_capitalisation_follows_turkic_case_mapping`); Format > Change Case follows the
|
||||
same Turkic mapping from each run's language, while other languages keep the Unicode default
|
||||
(`change_case_follows_turkic_case_mapping`, #3479).
|
||||
- Names of days are capitalised (`monday` → `Monday`). AutoCorrect Options (#412,
|
||||
`tools.autocorrect`, saved per user) switch each of these off: replacements, sentence and
|
||||
table-cell capitals, days, smart quotes, fractions, ordinals, dashes, links, bulleted and
|
||||
|
||||
Reference in New Issue
Block a user