mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 11:55:23 +00:00
Part of #4936: check spelling per paragraph, given ids in paint order; dictionary sources
- text.spelling.check gives craft-proof one paragraph at a time, as check_text_with expects: its language detection reads a paragraph, so a Vietnamese paragraph no longer leaves the English ones after it unchecked, and the check stops at the paragraph where the 10,000-word list fills up instead of proofing the rest of the text first. - Given `ids` are checked in paint order, as the result promises, not in the order they were passed. - Docs: English words come from the first dictionary craft-proof finds (dictionaries folder Hunspell, SCOWL from craft-i18n, system Hunspell), with the Moby word list as the last fallback, not from Moby alone. - Doc comments on the command functions and test helpers.
This commit is contained in:
@@ -8,7 +8,7 @@ use std::ops::Range;
|
||||
|
||||
use craft_proof::{IssueKind, Options, ProofLang};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::NodeKind;
|
||||
use vectorcraft_doc::{Document, NodeId, NodeKind};
|
||||
|
||||
use super::menucmds::whole_or;
|
||||
use super::typemenu::{all_texts, text_ids};
|
||||
@@ -23,6 +23,7 @@ pub const MAX_SUGGESTIONS: u64 = 20;
|
||||
/// Longest word suggestions are looked for, in bytes: the search costs more with the length.
|
||||
const MAX_WORD_LEN: usize = 128;
|
||||
|
||||
/// The Edit › Spelling commands.
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
@@ -30,7 +31,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Check Spelling",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [id…] (type objects, or groups holding them; default: every visible, unlocked type object)} the words the dictionaries don't know, each in its run's language (English when it has none; none in a language without a dictionary), in paint order → {words: [{id, start: byte, end: byte, word, lang}…], more: bool (more than 10000 words, the rest left out)}. text.editRange {id, start, end, insert} changes one",
|
||||
"{ids?: [id…] (type objects, or groups holding them; default: every visible, unlocked type object)} the words the dictionaries don't know, each in its run's language (English when it has none; none in a language without a dictionary), each paragraph on its own, in paint order (given ids too) → {words: [{id, start: byte, end: byte, word, lang}…], more: bool (more than 10000 words, the rest left out)}. text.editRange {id, start, end, insert} changes one",
|
||||
has_doc,
|
||||
check
|
||||
),
|
||||
@@ -46,12 +47,14 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
]
|
||||
}
|
||||
|
||||
/// `text.spelling.check`: the unknown words of the given type objects (or every visible, unlocked
|
||||
/// one), in paint order, each paragraph checked on its own, at most [`MAX_WORDS`] of them.
|
||||
fn check(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.spelling.check";
|
||||
// Given `ids` are checked like Find and Replace's, so a malformed one is an error rather than
|
||||
// a check of the whole document.
|
||||
let ids = match p.get("ids").filter(|v| !v.is_null()) {
|
||||
Some(_) => text_ids(&s.doc()?.doc, &checked_ids_param(s, p, "ids", C)?),
|
||||
Some(_) => in_paint_order(&s.doc()?.doc, text_ids(&s.doc()?.doc, &checked_ids_param(s, p, "ids", C)?)),
|
||||
None => all_texts(&s.doc()?.doc),
|
||||
};
|
||||
let st = s.doc()?;
|
||||
@@ -69,21 +72,47 @@ fn check(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
spans.push((at..end, ProofLang::from_tag(r.style.language.as_deref())));
|
||||
at = end;
|
||||
}
|
||||
for issue in craft_proof::check_text_with(&text, &spans, &opts, false, false) {
|
||||
if issue.kind != IssueKind::Spelling {
|
||||
continue;
|
||||
// craft-proof checks one paragraph at a time (its language detection reads the
|
||||
// paragraph), and the check stops at the paragraph where the list fills up rather than
|
||||
// proofing the rest of the text.
|
||||
let mut para_start = 0;
|
||||
for para in text.split('\n') {
|
||||
let para_end = para_start + para.len();
|
||||
let para_spans: Vec<(Range<usize>, ProofLang)> = spans
|
||||
.iter()
|
||||
.filter_map(|(r, lang)| {
|
||||
let (a, b) = (r.start.max(para_start), r.end.min(para_end));
|
||||
(a < b).then(|| (a - para_start..b - para_start, *lang))
|
||||
})
|
||||
.collect();
|
||||
for issue in craft_proof::check_text_with(para, ¶_spans, &opts, false, false) {
|
||||
if issue.kind != IssueKind::Spelling {
|
||||
continue;
|
||||
}
|
||||
let (start, end) = (para_start + issue.start, para_start + issue.end);
|
||||
let Some(word) = text.get(start..end) else { continue };
|
||||
if words.len() == MAX_WORDS {
|
||||
more = true;
|
||||
break 'texts;
|
||||
}
|
||||
words.push(json!({ "id": id.0, "start": start, "end": end, "word": word, "lang": issue.lang.tag() }));
|
||||
}
|
||||
let Some(word) = text.get(issue.start..issue.end) else { continue };
|
||||
if words.len() == MAX_WORDS {
|
||||
more = true;
|
||||
break 'texts;
|
||||
}
|
||||
words.push(json!({ "id": id.0, "start": issue.start, "end": issue.end, "word": word, "lang": issue.lang.tag() }));
|
||||
// Past the paragraph and its `\n`.
|
||||
para_start = para_end + 1;
|
||||
}
|
||||
}
|
||||
Ok(json!({ "words": words, "more": more }))
|
||||
}
|
||||
|
||||
/// `ids` sorted back to front, as the document paints them: by their index paths, which are in
|
||||
/// paint order.
|
||||
fn in_paint_order(d: &Document, mut ids: Vec<NodeId>) -> Vec<NodeId> {
|
||||
let paths = d.index_paths(ids.iter().copied());
|
||||
ids.sort_by(|a, b| paths.get(a).cmp(&paths.get(b)));
|
||||
ids
|
||||
}
|
||||
|
||||
/// `text.spelling.suggest`: up to `max` of the words the dictionary of `lang` suggests for `word`.
|
||||
fn suggest(_s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.spelling.suggest";
|
||||
let word = str_param(p, "word").map(str::trim).filter(|w| !w.is_empty()).ok_or_else(|| bad(C, "missing `word`"))?;
|
||||
|
||||
@@ -5,16 +5,19 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// A session with a new 400 × 400 document.
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// Runs a command that must succeed and gives its result.
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
s.execute(id, &p).unwrap()
|
||||
}
|
||||
|
||||
/// New point type at (10, `y`) holding `t`.
|
||||
fn text(s: &mut Session, y: f64, t: &str) -> NodeId {
|
||||
NodeId(run(s, "text.create", json!({"x": 10, "y": y, "text": t}))["id"].as_u64().unwrap())
|
||||
}
|
||||
@@ -60,6 +63,8 @@ fn check_goes_through_visible_unlocked_type_in_paint_order_or_the_ids_given() {
|
||||
assert_eq!(ids, [a.0, b.0], "hidden and locked type are left out");
|
||||
assert_eq!(unknown(&mut s, json!({"ids": [b.0]})), [(b.0, 7, 12, "wvxzq".to_string())]);
|
||||
assert_eq!(unknown(&mut s, json!({"ids": null})).len(), 2, "null ids is the default");
|
||||
let both = [(a.0, 6, 11, "qzxvw".to_string()), (b.0, 7, 12, "wvxzq".to_string())];
|
||||
assert_eq!(unknown(&mut s, json!({"ids": [b.0, a.0]})), both, "given ids are listed in paint order too");
|
||||
for ids in [json!(b.0), json!("all"), json!({"id": b.0}), json!([-1]), json!(["x"]), json!([999_999])] {
|
||||
assert!(s.execute("text.spelling.check", &json!({"ids": ids})).is_err(), "malformed ids {ids} is refused");
|
||||
}
|
||||
@@ -87,3 +92,14 @@ fn suggest_offers_dictionary_words() {
|
||||
assert!(s.execute("text.spelling.suggest", &json!({"word": "drawng", "max": max})).is_err(), "max {max}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_paragraph_is_checked_on_its_own_at_the_bytes_of_the_whole_text() {
|
||||
let mut s = session();
|
||||
// Read as one block, the Vietnamese first paragraph would make the whole text Vietnamese,
|
||||
// which has no dictionary, and the English typo below it would go unmarked.
|
||||
let vi = "Cảm ơn những người bạn";
|
||||
let t = text(&mut s, 50.0, &format!("{vi}\ndrawing qzxvw tools"));
|
||||
let at = vi.len() + 1 + "drawing ".len();
|
||||
assert_eq!(unknown(&mut s, json!({})), [(t.0, at as u64, at as u64 + 5, "qzxvw".to_string())]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user