From 05b5c197aea8402fbb92a72b5138eb40f3a90b1e Mon Sep 17 00:00:00 2001 From: Lishid Date: Mon, 19 Oct 2020 07:48:16 -0400 Subject: [PATCH] feat: Expose renderer spellcheck API (#25060) --- docs/api/web-frame.md | 14 +++++++ shell/renderer/api/electron_api_web_frame.cc | 39 ++++++++++++++++++++ shell/renderer/electron_renderer_client.cc | 14 ++++++- shell/renderer/electron_renderer_client.h | 4 ++ spec-main/spellchecker-spec.ts | 19 +++++++++- 5 files changed, 88 insertions(+), 2 deletions(-) diff --git a/docs/api/web-frame.md b/docs/api/web-frame.md index da688f18d3b3..2cf00b71f7a1 100644 --- a/docs/api/web-frame.md +++ b/docs/api/web-frame.md @@ -257,6 +257,20 @@ renderer process. Returns `WebFrame` - that has the supplied `routingId`, `null` if not found. +### `webFrame.isWordMisspelled(word)` + +* `word` String - The word to be spellchecked. + +Returns `Boolean` - True if the word is misspelled according to the built in +spellchecker, false otherwise. If no dictionary is loaded, always return false. + +### `webFrame.getWordSuggestions(word)` + +* `word` String - The misspelled word. + +Returns `String[]` - A list of suggested words for a given word. If the word +is spelled correctly, the result will be empty. + ## Properties ### `webFrame.top` _Readonly_ diff --git a/shell/renderer/api/electron_api_web_frame.cc b/shell/renderer/api/electron_api_web_frame.cc index 6fdb04ce5458..8db19a16c7cf 100644 --- a/shell/renderer/api/electron_api_web_frame.cc +++ b/shell/renderer/api/electron_api_web_frame.cc @@ -10,6 +10,8 @@ #include "base/command_line.h" #include "base/memory/memory_pressure_listener.h" +#include "base/strings/utf_string_conversions.h" +#include "components/spellcheck/renderer/spellcheck.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame_observer.h" #include "content/public/renderer/render_frame_visitor.h" @@ -24,6 +26,7 @@ #include "shell/common/node_includes.h" #include "shell/common/options_switches.h" #include "shell/renderer/api/electron_api_spell_check_client.h" +#include "shell/renderer/electron_renderer_client.h" #include "third_party/blink/public/common/page/page_zoom.h" #include "third_party/blink/public/common/web_cache/web_cache_resource_type_stats.h" #include "third_party/blink/public/platform/web_cache.h" @@ -101,6 +104,24 @@ content::RenderFrame* GetRenderFrame(v8::Local value) { return content::RenderFrame::FromWebFrame(frame); } +bool SpellCheckWord(v8::Isolate* isolate, + v8::Local window, + const std::string& word, + std::vector* optional_suggestions) { + size_t start; + size_t length; + + ElectronRendererClient* client = ElectronRendererClient::Get(); + auto* render_frame = GetRenderFrame(window); + if (!render_frame) + return true; + + base::string16 w = base::UTF8ToUTF16(word); + int id = render_frame->GetRoutingID(); + return client->GetSpellCheck()->SpellCheckWord( + w.c_str(), 0, word.size(), id, &start, &length, optional_suggestions); +} + class RenderFrameStatus final : public content::RenderFrameObserver { public: explicit RenderFrameStatus(content::RenderFrame* render_frame) @@ -671,6 +692,20 @@ blink::WebCacheResourceTypeStats GetResourceUsage(v8::Isolate* isolate) { return stats; } +bool IsWordMisspelled(v8::Isolate* isolate, + v8::Local window, + const std::string& word) { + return !SpellCheckWord(isolate, window, word, nullptr); +} + +std::vector GetWordSuggestions(v8::Isolate* isolate, + v8::Local window, + const std::string& word) { + std::vector suggestions; + SpellCheckWord(isolate, window, word, &suggestions); + return suggestions; +} + void ClearCache(v8::Isolate* isolate) { isolate->IdleNotificationDeadline(0.5); blink::WebCache::Clear(); @@ -826,6 +861,10 @@ void Initialize(v8::Local exports, &ExecuteJavaScriptInIsolatedWorld); dict.SetMethod("setIsolatedWorldInfo", &SetIsolatedWorldInfo); dict.SetMethod("getResourceUsage", &GetResourceUsage); +#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER) + dict.SetMethod("isWordMisspelled", &IsWordMisspelled); + dict.SetMethod("getWordSuggestions", &GetWordSuggestions); +#endif dict.SetMethod("clearCache", &ClearCache); dict.SetMethod("_findFrameByRoutingId", &FindFrameByRoutingId); dict.SetMethod("_getFrameForSelector", &GetFrameForSelector); diff --git a/shell/renderer/electron_renderer_client.cc b/shell/renderer/electron_renderer_client.cc index 67fd46c8add5..03e2cbe1a5c4 100644 --- a/shell/renderer/electron_renderer_client.cc +++ b/shell/renderer/electron_renderer_client.cc @@ -34,15 +34,27 @@ bool IsDevToolsExtension(content::RenderFrame* render_frame) { } // namespace +// static +ElectronRendererClient* ElectronRendererClient::self_ = nullptr; + ElectronRendererClient::ElectronRendererClient() : node_bindings_( NodeBindings::Create(NodeBindings::BrowserEnvironment::RENDERER)), - electron_bindings_(new ElectronBindings(node_bindings_->uv_loop())) {} + electron_bindings_(new ElectronBindings(node_bindings_->uv_loop())) { + DCHECK(!self_) << "Cannot have two ElectronRendererClient"; + self_ = this; +} ElectronRendererClient::~ElectronRendererClient() { asar::ClearArchives(); } +// static +ElectronRendererClient* ElectronRendererClient::Get() { + DCHECK(self_); + return self_; +} + void ElectronRendererClient::RenderFrameCreated( content::RenderFrame* render_frame) { new ElectronRenderFrameObserver(render_frame, this); diff --git a/shell/renderer/electron_renderer_client.h b/shell/renderer/electron_renderer_client.h index d3741d4470af..2c234dcb0ffc 100644 --- a/shell/renderer/electron_renderer_client.h +++ b/shell/renderer/electron_renderer_client.h @@ -26,6 +26,8 @@ class ElectronRendererClient : public RendererClientBase { ElectronRendererClient(); ~ElectronRendererClient() override; + static ElectronRendererClient* Get(); + // electron::RendererClientBase: void DidCreateScriptContext(v8::Handle context, content::RenderFrame* render_frame) override; @@ -70,6 +72,8 @@ class ElectronRendererClient : public RendererClientBase { // assertion, so we have to keep a book of injected web frames. std::set injected_frames_; + static ElectronRendererClient* self_; + DISALLOW_COPY_AND_ASSIGN(ElectronRendererClient); }; diff --git a/spec-main/spellchecker-spec.ts b/spec-main/spellchecker-spec.ts index 4b90d2286c8c..d3905df1b009 100644 --- a/spec-main/spellchecker-spec.ts +++ b/spec-main/spellchecker-spec.ts @@ -13,7 +13,10 @@ ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', () => { beforeEach(async () => { w = new BrowserWindow({ - show: false + show: false, + webPreferences: { + nodeIntegration: true + } }); await w.loadFile(path.resolve(__dirname, './fixtures/chromium/spellchecker.html')); }); @@ -62,6 +65,20 @@ ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', () => { expect(contextMenuParams.dictionarySuggestions).to.have.length.of.at.least(1); }); + ifit(shouldRun)('should expose webFrame spellchecker correctly', async () => { + await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "Beautifulllll asd asd"'); + await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()'); + // Wait for spellchecker to load + await delay(500); + + const callWebFrameFn = (expr: string) => w.webContents.executeJavaScript('require("electron").webFrame.' + expr); + + expect(await callWebFrameFn('isWordMisspelled("test")')).to.equal(false); + expect(await callWebFrameFn('isWordMisspelled("testt")')).to.equal(true); + expect(await callWebFrameFn('getWordSuggestions("test")')).to.be.empty(); + expect(await callWebFrameFn('getWordSuggestions("testt")')).to.not.be.empty(); + }); + describe('custom dictionary word list API', () => { let ses: Session;