mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-09 20:12:57 +00:00
Lowercase list items no longer get a grammar underline (storytold/wordcraft#1741) (storytold/wordcraft#1783)
* storytold/wordcraft#1741: WIP * storytold/wordcraft#1741: list items no longer get the sentence-capital grammar squiggle The English "Capitalize the first word of a sentence" rule skips the first word of a list paragraph (own or style numbering); later sentences are still checked. check_text_with and paragraph_issues take a list_item flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * storytold/wordcraft#1741: review fixes Thread the capital-first flag into the Polish, Croatian, Italian, Turkish and Portuguese grammar rules; test the drawn squiggles through layout() for own, style and num 0 numbering; tidy the proof test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * storytold/wordcraft#1741: main's Vietnamese proofing test passes the new list-item argument Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * storytold/wordcraft#1741: the third call in main's Vietnamese proofing test Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5fc80099e7
commit
de00c8a16a
8 files changed
+114
-30
No files matched your search
@@ -51,11 +51,17 @@ fn ends_sentence(gap: &str, prev: &str) -> bool {
|
||||
|
||||
/// Grammar issues in a paragraph of Croatian text, in order.
|
||||
pub fn check(text: &str) -> Vec<Issue> {
|
||||
check_with(text, true)
|
||||
}
|
||||
|
||||
/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital
|
||||
/// (false for a list item, which may be a fragment).
|
||||
pub fn check_with(text: &str, capital_first: bool) -> Vec<Issue> {
|
||||
let ws: Vec<(usize, usize)> = words(text);
|
||||
let mut v = Vec::new();
|
||||
repeated_words(text, &ws, &mut v);
|
||||
spaces(text, &mut v);
|
||||
sentence_capitals(text, &ws, &mut v);
|
||||
sentence_capitals(text, &ws, capital_first, &mut v);
|
||||
doubled_punctuation(text, &mut v);
|
||||
v.sort_by_key(|i| (i.start, i.end));
|
||||
v
|
||||
@@ -107,14 +113,14 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec<Issue>) {
|
||||
// List items (`– maçãs,`) may start in lower case.
|
||||
let list_item = text.trim_end().ends_with([',', ';']);
|
||||
for (k, &(a, b)) in ws.iter().enumerate() {
|
||||
let Some(w) = text.get(a..b) else { continue };
|
||||
let starts = if k == 0 {
|
||||
let before = text.get(..a).unwrap_or("");
|
||||
!list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c))
|
||||
capital_first && !list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c))
|
||||
} else {
|
||||
let Some(&(pa, pb)) = ws.get(k - 1) else { continue };
|
||||
ends_sentence(text.get(pb..a).unwrap_or(""), text.get(pa..pb).unwrap_or(""))
|
||||
|
||||
@@ -208,8 +208,8 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
|
||||
let mut after_end = true;
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec<Issue>) {
|
||||
let mut after_end = capital_first;
|
||||
for (a, b) in ws {
|
||||
let Some(w) = text.get(*a..*b) else { continue };
|
||||
if after_end && w.chars().next().is_some_and(char::is_lowercase) && w.chars().all(char::is_alphabetic) {
|
||||
@@ -327,11 +327,17 @@ fn un_apostrophe(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
|
||||
|
||||
/// Grammar issues in a paragraph of Italian text, in order.
|
||||
pub fn check(text: &str) -> Vec<Issue> {
|
||||
check_with(text, true)
|
||||
}
|
||||
|
||||
/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital
|
||||
/// (false for a list item, which may be a fragment).
|
||||
pub fn check_with(text: &str, capital_first: bool) -> Vec<Issue> {
|
||||
let ws: Vec<(usize, usize)> = words(text);
|
||||
let mut v = Vec::new();
|
||||
repeated_words(text, &ws, &mut v);
|
||||
spaces(text, &mut v);
|
||||
sentence_capitals(text, &ws, &mut v);
|
||||
sentence_capitals(text, &ws, capital_first, &mut v);
|
||||
doubled_punctuation(text, &mut v);
|
||||
po(text, &ws, &mut v);
|
||||
qual_e(text, &ws, &mut v);
|
||||
|
||||
+58
-11
@@ -653,14 +653,21 @@ pub fn suggest_in(word: &str, max: usize, lang: ProofLang) -> Vec<String> {
|
||||
/// ([`pl::grammar`]), the Portuguese ones ([`pt::grammar`]), the Italian ones ([`it::grammar`]),
|
||||
/// the Turkish ones ([`tr::grammar`]), Czech and Indonesian generic checks ([`cs::grammar`], [`id::grammar`]), or none for a language WordCraft doesn't proof.
|
||||
pub fn check_grammar_in(text: &str, lang: ProofLang) -> Vec<Issue> {
|
||||
grammar_in(text, lang, true)
|
||||
}
|
||||
|
||||
/// [`check_grammar_in`] where `capital_first` says whether the first word must be capitalized
|
||||
/// (false for a list item).
|
||||
fn grammar_in(text: &str, lang: ProofLang, capital_first: bool) -> Vec<Issue> {
|
||||
match lang {
|
||||
ProofLang::En => check_grammar(text),
|
||||
ProofLang::Pl => pl::grammar::check(text),
|
||||
ProofLang::Hr => hr::grammar::check(text),
|
||||
ProofLang::Pt => pt::grammar::check(text),
|
||||
ProofLang::It => it::grammar::check(text),
|
||||
ProofLang::Tr => tr::grammar::check(text),
|
||||
ProofLang::Ru => ru::grammar::check(text),
|
||||
ProofLang::En => grammar_en(text, capital_first),
|
||||
ProofLang::Pl => pl::grammar::check_with(text, capital_first),
|
||||
ProofLang::Hr => hr::grammar::check_with(text, capital_first),
|
||||
ProofLang::Pt => pt::grammar::check_with(text, capital_first),
|
||||
ProofLang::It => it::grammar::check_with(text, capital_first),
|
||||
ProofLang::Tr => tr::grammar::check_with(text, capital_first),
|
||||
ProofLang::Ru => ru::grammar::check_with(text, capital_first),
|
||||
// Czech has no sentence-capital rule, so a list item needs no exception.
|
||||
ProofLang::Cs => cs::grammar::check(text),
|
||||
ProofLang::Id => id::grammar::check(text),
|
||||
ProofLang::Bg | ProofLang::Other => Vec::new(),
|
||||
@@ -674,12 +681,15 @@ pub fn check_grammar_in(text: &str, lang: ProofLang) -> Vec<Issue> {
|
||||
/// inside that language's spans: an English quotation in a Polish paragraph gets English
|
||||
/// spelling and is never told a Polish rule.
|
||||
pub fn check_text(text: &str, spans: &[(Range<usize>, ProofLang)]) -> Vec<Issue> {
|
||||
check_text_with(text, spans, &Options::default(), true)
|
||||
check_text_with(text, spans, &Options::default(), true, false)
|
||||
}
|
||||
|
||||
/// [`check_text`] with the File › Options › Proofing choices: what spelling skips, and whether
|
||||
/// grammar is checked at all.
|
||||
pub fn check_text_with(text: &str, spans: &[(Range<usize>, ProofLang)], opts: &Options, grammar: bool) -> Vec<Issue> {
|
||||
///
|
||||
/// `list_item` marks a list paragraph: items are often fragments, so the English rule asking for
|
||||
/// a capital on the paragraph's first word is off for it (later sentences are still checked).
|
||||
pub fn check_text_with(text: &str, spans: &[(Range<usize>, ProofLang)], opts: &Options, grammar: bool, list_item: bool) -> Vec<Issue> {
|
||||
// Sorted, empty ones dropped, neighbours of one language merged.
|
||||
let mut sorted: Vec<&(Range<usize>, ProofLang)> = spans.iter().filter(|(r, _)| r.start < r.end).collect();
|
||||
sorted.sort_by_key(|s| s.0.start);
|
||||
@@ -723,7 +733,8 @@ pub fn check_text_with(text: &str, spans: &[(Range<usize>, ProofLang)], opts: &O
|
||||
langs.sort();
|
||||
langs.dedup();
|
||||
for lang in langs {
|
||||
v.extend(check_grammar_in(text, lang).into_iter().filter(|i| lang_at(i.start) == lang));
|
||||
let found = grammar_in(text, lang, !list_item);
|
||||
v.extend(found.into_iter().filter(|i| lang_at(i.start) == lang));
|
||||
}
|
||||
v.sort_by_key(|i| (i.start, i.end));
|
||||
v
|
||||
@@ -731,6 +742,12 @@ pub fn check_text_with(text: &str, spans: &[(Range<usize>, ProofLang)], opts: &O
|
||||
|
||||
/// Grammar issues in a paragraph of English text.
|
||||
pub fn check_grammar(text: &str) -> Vec<Issue> {
|
||||
grammar_en(text, true)
|
||||
}
|
||||
|
||||
/// The English rules; `capital_first` is whether the text's first word must start a sentence
|
||||
/// with a capital (false for a list item, which may be a fragment).
|
||||
fn grammar_en(text: &str, capital_first: bool) -> Vec<Issue> {
|
||||
let mut v = Vec::new();
|
||||
let ws = words(text);
|
||||
for pair in ws.windows(2) {
|
||||
@@ -803,7 +820,7 @@ pub fn check_grammar(text: &str) -> Vec<Issue> {
|
||||
});
|
||||
}
|
||||
// Sentence start capital.
|
||||
let mut after_end = true;
|
||||
let mut after_end = capital_first;
|
||||
for (a, b) in &ws {
|
||||
let Some(w) = text.get(*a..*b) else { continue };
|
||||
if after_end && w.chars().next().is_some_and(char::is_lowercase) && w.chars().all(char::is_alphabetic) {
|
||||
@@ -892,6 +909,36 @@ mod tests {
|
||||
assert!(is_correct_in("chrząszcz", ProofLang::Pl) && !is_correct_in("chrzonszcz", ProofLang::Pl));
|
||||
}
|
||||
|
||||
/// #1741: a list item may be a fragment, so its first word needs no capital in any language
|
||||
/// with the rule; a plain paragraph's does, and later sentences in the item are still checked.
|
||||
#[test]
|
||||
fn list_item_first_word_needs_no_capital() {
|
||||
let grammar = |t: &str, lang: ProofLang, list: bool| -> Vec<String> {
|
||||
check_text_with(t, &[(0..t.len(), lang)], &Options::default(), true, list)
|
||||
.into_iter()
|
||||
.filter(|i| i.kind == IssueKind::Grammar)
|
||||
.filter_map(|i| t.get(i.start..i.end).map(str::to_string))
|
||||
.collect()
|
||||
};
|
||||
for (lang, text) in [
|
||||
(ProofLang::En, "item one"),
|
||||
(ProofLang::Pl, "jabłka i gruszki"),
|
||||
(ProofLang::Hr, "jabuke i kruške"),
|
||||
(ProofLang::It, "mele e pere"),
|
||||
(ProofLang::Tr, "elma ve armut"),
|
||||
(ProofLang::Pt, "maçãs e peras"),
|
||||
(ProofLang::Ru, "яблоки и груши"),
|
||||
] {
|
||||
let first = text.split(' ').next().unwrap();
|
||||
assert!(grammar(text, lang, true).is_empty(), "{lang:?} list item: {:?}", grammar(text, lang, true));
|
||||
assert_eq!(grammar(text, lang, false), [first], "{lang:?} plain paragraph");
|
||||
}
|
||||
assert_eq!(grammar("hello world.", ProofLang::En, false), ["hello"]);
|
||||
// Later sentences in a list item are still checked.
|
||||
assert_eq!(grammar("item one. then two", ProofLang::En, true), ["then"]);
|
||||
assert_eq!(grammar("elma ve armut. sonra kiraz", ProofLang::Tr, true), ["sonra"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dictionary_loads() {
|
||||
assert!(dict::Dictionary::en_us().len() > 100_000);
|
||||
|
||||
@@ -245,11 +245,17 @@ fn is_consonant(c: char) -> bool {
|
||||
|
||||
/// Grammar issues in a paragraph of Polish text, in order.
|
||||
pub fn check(text: &str) -> Vec<Issue> {
|
||||
check_with(text, true)
|
||||
}
|
||||
|
||||
/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital
|
||||
/// (false for a list item, which may be a fragment).
|
||||
pub fn check_with(text: &str, capital_first: bool) -> Vec<Issue> {
|
||||
let ws: Vec<(usize, usize)> = words(text);
|
||||
let mut v = Vec::new();
|
||||
repeated_words(text, &ws, &mut v);
|
||||
spaces(text, &mut v);
|
||||
sentence_capitals(text, &ws, &mut v);
|
||||
sentence_capitals(text, &ws, capital_first, &mut v);
|
||||
missing_commas(text, &ws, &mut v);
|
||||
we_ze(text, &ws, &mut v);
|
||||
doubled_punctuation(text, &mut v);
|
||||
@@ -330,14 +336,14 @@ fn ends_sentence(gap: &str, prev: &str) -> bool {
|
||||
rest.starts_with(char::is_whitespace) && rest.trim_start().chars().all(|c| OPENING.contains(&c))
|
||||
}
|
||||
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec<Issue>) {
|
||||
// List items (`– jabłka,`) may start in lower case.
|
||||
let list_item = text.trim_end().ends_with([',', ';']);
|
||||
for (k, &(a, b)) in ws.iter().enumerate() {
|
||||
let Some(w) = text.get(a..b) else { continue };
|
||||
let starts = if k == 0 {
|
||||
let before = text.get(..a).unwrap_or("");
|
||||
!list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c))
|
||||
capital_first && !list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c))
|
||||
} else {
|
||||
let Some(&(pa, pb)) = ws.get(k - 1) else { continue };
|
||||
ends_sentence(text.get(pb..a).unwrap_or(""), text.get(pa..pb).unwrap_or(""))
|
||||
|
||||
@@ -68,11 +68,17 @@ fn ends_sentence(gap: &str, prev: &str) -> bool {
|
||||
|
||||
/// Grammar issues in a paragraph of Portuguese text, in order.
|
||||
pub fn check(text: &str) -> Vec<Issue> {
|
||||
check_with(text, true)
|
||||
}
|
||||
|
||||
/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital
|
||||
/// (false for a list item, which may be a fragment).
|
||||
pub fn check_with(text: &str, capital_first: bool) -> Vec<Issue> {
|
||||
let ws: Vec<(usize, usize)> = words(text);
|
||||
let mut v = Vec::new();
|
||||
repeated_words(text, &ws, &mut v);
|
||||
spaces(text, &mut v);
|
||||
sentence_capitals(text, &ws, &mut v);
|
||||
sentence_capitals(text, &ws, capital_first, &mut v);
|
||||
doubled_punctuation(text, &mut v);
|
||||
v.sort_by_key(|i| (i.start, i.end));
|
||||
v
|
||||
@@ -124,14 +130,14 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec<Issue>) {
|
||||
// List items (`– maçãs,`) may start in lower case.
|
||||
let list_item = text.trim_end().ends_with([',', ';']);
|
||||
for (k, &(a, b)) in ws.iter().enumerate() {
|
||||
let Some(w) = text.get(a..b) else { continue };
|
||||
let starts = if k == 0 {
|
||||
let before = text.get(..a).unwrap_or("");
|
||||
!list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c))
|
||||
capital_first && !list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c))
|
||||
} else {
|
||||
let Some(&(pa, pb)) = ws.get(k - 1) else { continue };
|
||||
ends_sentence(text.get(pb..a).unwrap_or(""), text.get(pa..pb).unwrap_or(""))
|
||||
|
||||
@@ -441,11 +441,17 @@ fn capitalize(w: &str) -> String {
|
||||
|
||||
/// Grammar issues in a paragraph of Russian text, in order.
|
||||
pub fn check(text: &str) -> Vec<Issue> {
|
||||
check_with(text, true)
|
||||
}
|
||||
|
||||
/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital
|
||||
/// (false for a list item, which may be a fragment).
|
||||
pub fn check_with(text: &str, capital_first: bool) -> Vec<Issue> {
|
||||
let ws: Vec<(usize, usize)> = words(text);
|
||||
let mut v = Vec::new();
|
||||
repeated_words(text, &ws, &mut v);
|
||||
spaces(text, &mut v);
|
||||
sentence_capitals(text, &ws, &mut v);
|
||||
sentence_capitals(text, &ws, capital_first, &mut v);
|
||||
missing_commas(text, &ws, &mut v);
|
||||
doubled_punctuation(text, &mut v);
|
||||
v.sort_by_key(|i| (i.start, i.end));
|
||||
@@ -525,9 +531,9 @@ fn ends_sentence(gap: &str, prev: &str) -> bool {
|
||||
rest.starts_with(char::is_whitespace) && rest.trim_start().chars().all(|c| OPENING.contains(&c))
|
||||
}
|
||||
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
|
||||
fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec<Issue>) {
|
||||
// List items (`– яблоки,`) may start in lower case.
|
||||
let list_item = text.trim_end().ends_with([',', ';']);
|
||||
let list_item = !capital_first || text.trim_end().ends_with([',', ';']);
|
||||
for (k, &(a, b)) in ws.iter().enumerate() {
|
||||
let Some(w) = text.get(a..b) else { continue };
|
||||
let starts = if k == 0 {
|
||||
|
||||
@@ -132,6 +132,12 @@ const CONJUNCTIONS_NO_POST_COMMA: &[&str] = &["ve", "veya", "yahut", "veyahut",
|
||||
|
||||
/// Check grammar and punctuation in a Turkish text block.
|
||||
pub fn check(text: &str) -> Vec<Issue> {
|
||||
check_with(text, true)
|
||||
}
|
||||
|
||||
/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital
|
||||
/// (false for a list item, which may be a fragment).
|
||||
pub fn check_with(text: &str, capital_first: bool) -> Vec<Issue> {
|
||||
let mut v = Vec::new();
|
||||
let ws = words(text);
|
||||
|
||||
@@ -240,7 +246,8 @@ pub fn check(text: &str) -> Vec<Issue> {
|
||||
for (i, &(w_start, w_end)) in ws.iter().enumerate() {
|
||||
if i == 0 {
|
||||
// First word in paragraph
|
||||
if let Some(w) = text.get(w_start..w_end)
|
||||
if capital_first
|
||||
&& let Some(w) = text.get(w_start..w_end)
|
||||
&& w.chars().next().is_some_and(char::is_lowercase)
|
||||
{
|
||||
let cap = turkish_capitalize(w);
|
||||
|
||||
@@ -78,15 +78,15 @@ mod checker_tests {
|
||||
fn vietnamese_paragraphs_under_english_get_no_marks() {
|
||||
let text = "Hôm nay tôi đi học, trời rất đẹp và mát mẻ.";
|
||||
let spans = [(0..text.len(), ProofLang::En)];
|
||||
assert!(check_text_with(text, &spans, &Options::default(), true).is_empty());
|
||||
assert!(check_text_with(text, &spans, &Options::default(), true, false).is_empty());
|
||||
// No language at all (the document default) behaves the same.
|
||||
assert!(check_text_with(text, &[], &Options::default(), true).is_empty());
|
||||
assert!(check_text_with(text, &[], &Options::default(), true, false).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn english_keeps_its_marks_and_skips_vietnamese_names() {
|
||||
let text = "We met Nguyễn yesterdy at the office.";
|
||||
let issues = check_text_with(text, &[(0..text.len(), ProofLang::En)], &Options::default(), false);
|
||||
let issues = check_text_with(text, &[(0..text.len(), ProofLang::En)], &Options::default(), false, false);
|
||||
let flagged: Vec<&str> = issues.iter().filter_map(|i| text.get(i.start..i.end)).collect();
|
||||
assert_eq!(flagged, ["yesterdy"]);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user