mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 10:27:44 -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 wordcraft_doc::{InlineObject, PartKind, Pos, RevisionKind, StoryRef};
|
||||||
|
|
||||||
use super::sel_result;
|
use super::sel_result;
|
||||||
|
use super::tools::{is_turkic, lower_char, upper_char};
|
||||||
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
|
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
|
||||||
|
|
||||||
/// Word's font size list.
|
/// 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 to = if path == b.path { b.off } else { p.len() };
|
||||||
let chars = live_chars(p, from, to);
|
let chars = live_chars(p, from, to);
|
||||||
let src: String = chars.iter().map(|(_, c)| c).collect();
|
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() {
|
if groups.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
@@ -587,37 +597,47 @@ fn is_title(t: &str) -> bool {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn convert(src: &str, mode: &str, para_start: bool) -> String {
|
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
|
/// 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).
|
/// change of length such as ß → SS or İ → i̇ can be applied run by run). `turkic` is one flag
|
||||||
fn convert_chars(src: &str, mode: &str, para_start: bool) -> Vec<String> {
|
/// 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();
|
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`).
|
// 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 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 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());
|
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.
|
// 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 up = mode == "title" || para_start;
|
||||||
let mut out = Vec::with_capacity(chars.len());
|
let mut out = Vec::with_capacity(chars.len());
|
||||||
for (i, &c) in chars.iter().enumerate() {
|
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 {
|
let new = match mode {
|
||||||
"upper" => c.to_uppercase().collect(),
|
"upper" => upper(c),
|
||||||
"lower" => lower(i, c),
|
"lower" => lower(i, c),
|
||||||
"toggle" => {
|
"toggle" => {
|
||||||
if c.is_uppercase() {
|
if c.is_uppercase() {
|
||||||
lower(i, c)
|
lower(i, c)
|
||||||
} else {
|
} else {
|
||||||
c.to_uppercase().collect()
|
upper(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"title" => {
|
"title" => {
|
||||||
let n = if up && c.is_alphabetic() {
|
let n = if up && c.is_alphabetic() {
|
||||||
up = false;
|
up = false;
|
||||||
c.to_uppercase().collect()
|
upper(c)
|
||||||
} else {
|
} else {
|
||||||
lower(i, c)
|
lower(i, c)
|
||||||
};
|
};
|
||||||
@@ -630,7 +650,7 @@ fn convert_chars(src: &str, mode: &str, para_start: bool) -> Vec<String> {
|
|||||||
// Sentence case.
|
// Sentence case.
|
||||||
let n = if up && c.is_alphabetic() {
|
let n = if up && c.is_alphabetic() {
|
||||||
up = false;
|
up = false;
|
||||||
c.to_uppercase().collect()
|
upper(c)
|
||||||
} else {
|
} else {
|
||||||
lower(i, c)
|
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).
|
/// 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 {
|
if wordcraft_proof::ProofLang::from_tag(lang) == wordcraft_proof::ProofLang::Tr {
|
||||||
return true; // tr, tur, any region, same normalisation as proofing
|
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.
|
/// 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' {
|
if turkic && c == 'i' {
|
||||||
return "\u{130}".to_string();
|
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`,
|
/// A letter in lowercase: `I` → `ı` in Turkic languages. `İ` is always the single letter `i`,
|
||||||
/// never `i` plus a combining dot.
|
/// 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 {
|
match c {
|
||||||
'I' if turkic => "\u{131}".to_string(),
|
'I' if turkic => "\u{131}".to_string(),
|
||||||
'\u{130}' => "i".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");
|
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
|
// 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
|
// 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.
|
// 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 `İ`
|
`İ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
|
gives the single letter `i` in every language, never `i` + combining dot. Other languages keep
|
||||||
the Unicode default (`istanbul` → `Istanbul` in English)
|
the Unicode default (`istanbul` → `Istanbul` in English)
|
||||||
(`autocorrect_capitalisation_follows_turkic_case_mapping`). Format > Change Case is not yet
|
(`autocorrect_capitalisation_follows_turkic_case_mapping`); Format > Change Case follows the
|
||||||
Turkish-aware.
|
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,
|
- Names of days are capitalised (`monday` → `Monday`). AutoCorrect Options (#412,
|
||||||
`tools.autocorrect`, saved per user) switch each of these off: replacements, sentence and
|
`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
|
table-cell capitals, days, smart quotes, fractions, ordinals, dashes, links, bulleted and
|
||||||
|
|||||||
Reference in New Issue
Block a user