CP6.13: tr grammar — emoticons, decimals and scores aren't stray spaces before punctuation; ordinals (5. sınıf) and file or web names (rapor.txt) don't end a sentence (CodeRabbit on craft-libs#484/#489)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
This commit is contained in:
Martin BallandClaude Opus 5.5 committed 2026-11-01 22:34:33 +00:00
1 parent 0b292e955c
commit 8d3a9366db
1 file changed
+43 -3
+43 -3
View File
@@ -172,9 +172,22 @@ pub fn check_with(text: &str, capital_first: bool) -> Vec<Issue> {
let (idx, c) = chars[i];
if matches!(c, ',' | '.' | '?' | '!' | ';' | ':') && i > 0 && chars[i - 1].1 == ' ' {
let prev_sp = chars[i - 1].0;
// Leave emoticons and ellipses alone
let is_dot_in_dots = c == '.' && (i + 1 < chars.len() && chars[i + 1].1 == '.');
if !is_dot_in_dots {
// Leave ellipses (` ...`), numbers (` .5`), emoticons (` :)`, ` ;-)`, ` :D`) and
// scores (`3 : 1`) alone.
let after = chars.get(i + 1).map(|&(_, a)| a);
let ratio = c == ':'
&& i >= 2
&& chars.get(i - 2).is_some_and(|&(_, b)| b.is_ascii_digit())
&& after == Some(' ')
&& chars.get(i + 2).is_some_and(|&(_, d)| d.is_ascii_digit());
let flag = !ratio
&& match c {
',' => true,
';' => !matches!(after, Some(')' | '(' | '-' | 'P' | 'D')),
'.' => !matches!(after, Some('.')) && !after.is_some_and(|a| a.is_ascii_digit()),
_ => after.is_none_or(|a| a.is_whitespace() || matches!(a, '"' | '”' | '’' | '!' | '?')),
};
if flag {
v.push(Issue {
start: prev_sp,
end: idx + c.len_utf8(),
@@ -268,7 +281,13 @@ pub fn check_with(text: &str, capital_first: bool) -> Vec<Issue> {
let prev_word = text.get(ws[i - 1].0..prev_end).unwrap_or("");
if let Some(gap) = text.get(prev_end..w_start) {
let trimmed = gap.trim();
// A space must follow the mark (`rapor.txt`, `www.example.com` are one token), and a
// number before a period is an ordinal (`5. sınıf`, `21. yüzyıl`).
let spaced = gap.rfind(['.', '?', '!']).and_then(|m| gap.get(m + 1..)).is_some_and(|rest| rest.contains(char::is_whitespace));
let ordinal = trimmed.ends_with('.') && prev_word.chars().any(|c| c.is_ascii_digit());
if (trimmed.ends_with('.') || trimmed.ends_with('?') || trimmed.ends_with('!'))
&& spaced
&& !ordinal
&& !trimmed.contains("...")
&& !ABBREVIATIONS.contains(&prev_word.to_lowercase().as_str())
&& let Some(w) = text.get(w_start..w_end)
@@ -310,6 +329,27 @@ pub fn turkish_capitalize(s: &str) -> String {
mod tests {
use super::*;
/// CodeRabbit on craft-libs#484/#489: emoticons, decimals and scores aren't stray spaces;
/// ordinals and file or web names don't end a sentence.
#[test]
fn emoticons_numbers_ordinals_and_names_are_left_alone() {
for ok in [
"Harika :)",
"Tamam ;)",
"Süper :D",
"Oran .5 oldu.",
"Maç 3 : 1 bitti.",
"Ali 5. sınıfta okuyor.",
"Bina 21. yüzyıldan kalma.",
"Dosyayı rapor.txt olarak kaydet.",
"Adres www.example.com olarak yazıldı.",
] {
assert!(check(ok).is_empty(), "{ok}: {:?}", check(ok));
}
assert_eq!(check("Gel , bak.").len(), 1, "a real stray space is still flagged");
assert_eq!(check("Geldi. bu iyi.").len(), 1, "a real sentence start is still flagged");
}
#[test]
fn clean_turkish_text_produces_no_issues() {
let text = "Bu bir test cümlesidir. Kitap ve defter aldım. Çocuk yavaş yavaş yürüdü.";