fix: contractions handling in spellchecker (#18506)

This fixes #18459 by improving the handling of contractions in the spellcheck API. Specifically, it now accepts contraction words where the spellchecker recognizes the whole word, and not, as previously, just if it recognizes all of its parts.
This commit is contained in:
Maya Wolf 2019-05-31 06:19:10 +02:00 committed by Shelley Vohr
parent a31faaae61
commit ab70e854f8
3 changed files with 51 additions and 45 deletions

View file

@ -5,6 +5,8 @@
#include "atom/renderer/api/atom_api_spell_check_client.h" #include "atom/renderer/api/atom_api_spell_check_client.h"
#include <map> #include <map>
#include <set>
#include <unordered_set>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -39,14 +41,16 @@ bool HasWordCharacters(const base::string16& text, int index) {
return false; return false;
} }
struct Word {
blink::WebTextCheckingResult result;
base::string16 text;
std::vector<base::string16> contraction_words;
};
} // namespace } // namespace
class SpellCheckClient::SpellcheckRequest { class SpellCheckClient::SpellcheckRequest {
public: public:
// Map of individual words to list of occurrences in text
using WordMap =
std::map<base::string16, std::vector<blink::WebTextCheckingResult>>;
SpellcheckRequest( SpellcheckRequest(
const base::string16& text, const base::string16& text,
std::unique_ptr<blink::WebTextCheckingCompletion> completion) std::unique_ptr<blink::WebTextCheckingCompletion> completion)
@ -55,11 +59,11 @@ class SpellCheckClient::SpellcheckRequest {
const base::string16& text() const { return text_; } const base::string16& text() const { return text_; }
blink::WebTextCheckingCompletion* completion() { return completion_.get(); } blink::WebTextCheckingCompletion* completion() { return completion_.get(); }
WordMap& wordmap() { return word_map_; } std::vector<Word>& wordlist() { return word_list_; }
private: private:
base::string16 text_; // Text to be checked in this task. base::string16 text_; // Text to be checked in this task.
WordMap word_map_; // WordMap to hold distinct words in text std::vector<Word> word_list_; // List of Words found in text
// The interface to send the misspelled ranges to WebKit. // The interface to send the misspelled ranges to WebKit.
std::unique_ptr<blink::WebTextCheckingCompletion> completion_; std::unique_ptr<blink::WebTextCheckingCompletion> completion_;
@ -150,9 +154,9 @@ void SpellCheckClient::SpellCheckText() {
base::string16 word; base::string16 word;
size_t word_start; size_t word_start;
size_t word_length; size_t word_length;
std::vector<base::string16> words; std::set<base::string16> words;
auto& word_map = pending_request_param_->wordmap(); auto& word_list = pending_request_param_->wordlist();
blink::WebTextCheckingResult result; Word word_entry;
for (;;) { // Run until end of text for (;;) { // Run until end of text
const auto status = const auto status =
text_iterator_.GetNextWord(&word, &word_start, &word_length); text_iterator_.GetNextWord(&word, &word_start, &word_length);
@ -161,23 +165,18 @@ void SpellCheckClient::SpellCheckText() {
if (status == SpellcheckWordIterator::IS_SKIPPABLE) if (status == SpellcheckWordIterator::IS_SKIPPABLE)
continue; continue;
result.location = base::checked_cast<int>(word_start); word_entry.result.location = base::checked_cast<int>(word_start);
result.length = base::checked_cast<int>(word_length); word_entry.result.length = base::checked_cast<int>(word_length);
word_entry.text = word;
word_entry.contraction_words.clear();
word_list.push_back(word_entry);
words.insert(word);
// If the given word is a concatenated word of two or more valid words // If the given word is a concatenated word of two or more valid words
// (e.g. "hello:hello"), we should treat it as a valid word. // (e.g. "hello:hello"), we should treat it as a valid word.
std::vector<base::string16> contraction_words; if (IsContraction(scope, word, &word_entry.contraction_words)) {
if (!IsContraction(scope, word, &contraction_words)) { for (const auto& w : word_entry.contraction_words) {
words.push_back(word); words.insert(w);
word_map[word].push_back(result);
} else {
// For a contraction, we want check the spellings of each individual
// part, but mark the entire word incorrect if any part is misspelled
// Hence, we use the same word_start and word_length values for every
// part of the contraction.
for (const auto& w : contraction_words) {
words.push_back(w);
word_map[w].push_back(result);
} }
} }
} }
@ -189,29 +188,35 @@ void SpellCheckClient::SpellCheckText() {
void SpellCheckClient::OnSpellCheckDone( void SpellCheckClient::OnSpellCheckDone(
const std::vector<base::string16>& misspelled_words) { const std::vector<base::string16>& misspelled_words) {
std::vector<blink::WebTextCheckingResult> results; std::vector<blink::WebTextCheckingResult> results;
std::unordered_set<base::string16> misspelled(misspelled_words.begin(),
misspelled_words.end());
auto& word_map = pending_request_param_->wordmap(); auto& word_list = pending_request_param_->wordlist();
// Take each word from the list of misspelled words received, find their for (const auto& word : word_list) {
// corresponding WebTextCheckingResult that's stored in the map and pass if (misspelled.find(word.text) != misspelled.end()) {
// all the results to blink through the completion callback. // If this is a contraction, iterate through parts and accept the word
for (const auto& word : misspelled_words) { // if none of them are misspelled
auto iter = word_map.find(word); if (!word.contraction_words.empty()) {
if (iter != word_map.end()) { auto all_correct = true;
// Word found in map, now gather all the occurrences of the word for (const auto& contraction_word : word.contraction_words) {
// from the map value if (misspelled.find(contraction_word) != misspelled.end()) {
auto& words = iter->second; all_correct = false;
results.insert(results.end(), words.begin(), words.end()); break;
words.clear(); }
}
if (all_correct)
continue;
}
results.push_back(word.result);
} }
} }
pending_request_param_->completion()->DidFinishCheckingText(results); pending_request_param_->completion()->DidFinishCheckingText(results);
pending_request_param_ = nullptr; pending_request_param_ = nullptr;
} }
void SpellCheckClient::SpellCheckWords( void SpellCheckClient::SpellCheckWords(const SpellCheckScope& scope,
const SpellCheckScope& scope, const std::set<base::string16>& words) {
const std::vector<base::string16>& words) {
DCHECK(!scope.spell_check_.IsEmpty()); DCHECK(!scope.spell_check_.IsEmpty());
v8::Local<v8::FunctionTemplate> templ = mate::CreateFunctionTemplate( v8::Local<v8::FunctionTemplate> templ = mate::CreateFunctionTemplate(

View file

@ -6,6 +6,7 @@
#define ATOM_RENDERER_API_ATOM_API_SPELL_CHECK_CLIENT_H_ #define ATOM_RENDERER_API_ATOM_API_SPELL_CHECK_CLIENT_H_
#include <memory> #include <memory>
#include <set>
#include <string> #include <string>
#include <vector> #include <vector>
@ -68,7 +69,7 @@ class SpellCheckClient : public blink::WebSpellCheckPanelHostClient,
// The javascript function will callback OnSpellCheckDone // The javascript function will callback OnSpellCheckDone
// with the results of all the misspelled words. // with the results of all the misspelled words.
void SpellCheckWords(const SpellCheckScope& scope, void SpellCheckWords(const SpellCheckScope& scope,
const std::vector<base::string16>& words); const std::set<base::string16>& words);
// Returns whether or not the given word is a contraction of valid words // Returns whether or not the given word is a contraction of valid words
// (e.g. "word:word"). // (e.g. "word:word").

View file

@ -40,19 +40,19 @@ describe('webFrame module', function () {
const spellCheckerFeedback = const spellCheckerFeedback =
new Promise(resolve => { new Promise(resolve => {
ipcMain.on('spec-spell-check', (e, words, callback) => { ipcMain.on('spec-spell-check', (e, words, callback) => {
if (words.length === 2) { if (words.length === 5) {
// The promise is resolved only after this event is received twice // The API calls the provider after every completed word.
// Array contains only 1 word first time and 2 the next time // The promise is resolved only after this event is received with all words.
resolve([words, callback]) resolve([words, callback])
} }
}) })
}) })
const inputText = 'spleling test ' const inputText = `spleling test you're `
for (const keyCode of inputText) { for (const keyCode of inputText) {
w.webContents.sendInputEvent({ type: 'char', keyCode }) w.webContents.sendInputEvent({ type: 'char', keyCode })
} }
const [words, callback] = await spellCheckerFeedback const [words, callback] = await spellCheckerFeedback
expect(words).to.deep.equal(['spleling', 'test']) expect(words.sort()).to.deep.equal(['spleling', 'test', `you're`, 'you', 're'].sort())
expect(callback).to.be.true() expect(callback).to.be.true()
}) })