From 502bb7c0b8198ce75a31a3df7c4463da238fbcc5 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Wed, 2 Sep 2015 13:20:01 -0700 Subject: [PATCH] feat(spellcheck): Custom spellcheck powered by our DraftStore extensions API Summary: New draft store extension that highlights misspelled words. Test Plan: No test coverage yet Reviewers: evan, dillon Reviewed By: evan Differential Revision: https://phab.nylas.com/D1972 --- .../lib/draft-extension.coffee | 105 +++++++++ .../composer-spellcheck/lib/main.coffee | 11 + .../composer-spellcheck/package.json | 17 ++ .../spec/draft-extension-spec.coffee | 33 +++ .../fixtures/california-spelling-lookup.json | 200 ++++++++++++++++++ .../california-with-misspellings-after.html | 1 + .../california-with-misspellings-before.html | 1 + .../lib/contenteditable-component.cjsx | 4 + .../composer/stylesheets/composer.less | 6 + .../message-list/lib/email-frame.cjsx | 2 +- 10 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 internal_packages/composer-spellcheck/lib/draft-extension.coffee create mode 100644 internal_packages/composer-spellcheck/lib/main.coffee create mode 100755 internal_packages/composer-spellcheck/package.json create mode 100644 internal_packages/composer-spellcheck/spec/draft-extension-spec.coffee create mode 100644 internal_packages/composer-spellcheck/spec/fixtures/california-spelling-lookup.json create mode 100644 internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-after.html create mode 100644 internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-before.html diff --git a/internal_packages/composer-spellcheck/lib/draft-extension.coffee b/internal_packages/composer-spellcheck/lib/draft-extension.coffee new file mode 100644 index 000000000..db81297ee --- /dev/null +++ b/internal_packages/composer-spellcheck/lib/draft-extension.coffee @@ -0,0 +1,105 @@ +{DraftStoreExtension, AccountStore} = require 'nylas-exports' +_ = require 'underscore' + +SpellcheckCache = {} + +class SpellcheckDraftStoreExtension extends DraftStoreExtension + + @isMisspelled: (word) -> + @spellchecker ?= require('spellchecker') + SpellcheckCache[word] ?= @spellchecker.isMisspelled(word) + SpellcheckCache[word] + + @ensureSetup: -> + @walkTreeNodes ?= [] + @walkTreesDebounced ?= _.debounce(@walkTrees, 200) + + @onInput: (editableNode, event) -> + @ensureSetup() + @walkTreeNodes.push(editableNode) + @walkTreesDebounced() + + @onSubstitutionPerformed: (editableNode) -> + @ensureSetup() + @walkTreeNodes.push(editableNode) + @walkTreesDebounced() + + @walkTrees: (nodes) => + _.each(_.uniq(@walkTreeNodes), @walkTree) + + @walkTree: (editableNode) => + treeWalker = document.createTreeWalker(editableNode, NodeFilter.SHOW_TEXT) + + nodeList = [] + selection = document.getSelection() + selectionSnapshot = + anchorNode: selection.anchorNode + anchorOffset: selection.anchorOffset + focusNode: selection.focusNode + focusOffset: selection.focusOffset + selectionImpacted = false + + while (treeWalker.nextNode()) + nodeList.push(treeWalker.currentNode) + + while (node = nodeList.pop()) + str = node.textContent + + # https://regex101.com/r/bG5yC4/1 + wordRegexp = /(\w[\w'’-]*\w|\w)/g + + while ((match = wordRegexp.exec(str)) isnt null) + spellingSpan = null + if node.parentNode and node.parentNode.nodeName is 'SPELLING' + if match[0] is str + spellingSpan = node.parentNode + else + node.parentNode.classList.remove('misspelled') + + misspelled = @isMisspelled(match[0]) + markedAsMisspelled = spellingSpan?.classList.contains('misspelled') + + if misspelled and not markedAsMisspelled + if spellingSpan + spellingSpan.classList.add('misspelled') + else + if match.index is 0 + matchNode = node + else + matchNode = node.splitText(match.index) + afterMatchNode = matchNode.splitText(match[0].length) + + spellingSpan = document.createElement('spelling') + spellingSpan.classList.add('misspelled') + spellingSpan.innerText = match[0] + matchNode.parentNode.replaceChild(spellingSpan, matchNode) + + for prop in ['anchor', 'focus'] + if selectionSnapshot["#{prop}Node"] is node + if selectionSnapshot["#{prop}Offset"] > match.index + match[0].length + selectionImpacted = true + selectionSnapshot["#{prop}Node"] = afterMatchNode + selectionSnapshot["#{prop}Offset"] -= match.index + match[0].length + else if selectionSnapshot["#{prop}Offset"] > match.index + selectionImpacted = true + selectionSnapshot["#{prop}Node"] = spellingSpan.childNodes[0] + selectionSnapshot["#{prop}Offset"] -= match.index + + nodeList.unshift(afterMatchNode) + break + + else if not misspelled and markedAsMisspelled + spellingSpan.classList.remove('misspelled') + + if selectionImpacted + selection.setBaseAndExtent(selectionSnapshot.anchorNode, selectionSnapshot.anchorOffset, selectionSnapshot.focusNode, selectionSnapshot.focusOffset) + + @finalizeSessionBeforeSending: (session) -> + body = session.draft().body + clean = body.replace(/<\/?spelling[^>]*>/g, '') + if body != clean + session.changes.add(body: clean) + +SpellcheckDraftStoreExtension.SpellcheckCache = SpellcheckCache + +module.exports = SpellcheckDraftStoreExtension diff --git a/internal_packages/composer-spellcheck/lib/main.coffee b/internal_packages/composer-spellcheck/lib/main.coffee new file mode 100644 index 000000000..9572b04a9 --- /dev/null +++ b/internal_packages/composer-spellcheck/lib/main.coffee @@ -0,0 +1,11 @@ +{ComponentRegistry, DraftStore} = require 'nylas-exports' +Extension = require './draft-extension' + +module.exports = + activate: (@state={}) -> + DraftStore.registerExtension(Extension) + + deactivate: -> + DraftStore.unregisterExtension(Extension) + + serialize: -> @state diff --git a/internal_packages/composer-spellcheck/package.json b/internal_packages/composer-spellcheck/package.json new file mode 100755 index 000000000..e3ec7baed --- /dev/null +++ b/internal_packages/composer-spellcheck/package.json @@ -0,0 +1,17 @@ +{ + "name": "composer-spellcheck", + "version": "0.1.0", + "main": "./lib/main", + "description": "A small extension to the draft store that implements spellcheck", + "license": "Proprietary", + "private": true, + "engines": { + "atom": "*" + }, + "windowTypes": { + "default": true, + "composer": true + }, + "dependencies": { + } +} diff --git a/internal_packages/composer-spellcheck/spec/draft-extension-spec.coffee b/internal_packages/composer-spellcheck/spec/draft-extension-spec.coffee new file mode 100644 index 000000000..1234d9414 --- /dev/null +++ b/internal_packages/composer-spellcheck/spec/draft-extension-spec.coffee @@ -0,0 +1,33 @@ +SpellcheckDraftStoreExtension = require '../lib/draft-extension' +fs = require 'fs' +_ = require 'underscore' + +initialHTML = fs.readFileSync(__dirname + '/fixtures/california-with-misspellings-before.html').toString() +expectedHTML = fs.readFileSync(__dirname + '/fixtures/california-with-misspellings-after.html').toString() + +describe "SpellcheckDraftStoreExtension", -> + beforeEach -> + # Avoid differences between node-spellcheck on different platforms + spellings = JSON.parse(fs.readFileSync(__dirname + '/fixtures/california-spelling-lookup.json')) + spyOn(SpellcheckDraftStoreExtension, 'isMisspelled').andCallFake (word) -> + spellings[word] + + describe "walkTree", -> + it "correctly walks a DOM tree and surrounds mispelled words", -> + dom = document.createElement('div') + dom.innerHTML = initialHTML + SpellcheckDraftStoreExtension.walkTree(dom) + expect(dom.innerHTML).toEqual(expectedHTML) + + describe "finalizeSessionBeforeSending", -> + it "removes the annotations it inserted", -> + session = + draft: -> + body: expectedHTML + changes: + add: jasmine.createSpy('add') + + SpellcheckDraftStoreExtension.finalizeSessionBeforeSending(session) + expect(session.changes.add).toHaveBeenCalledWith(body: initialHTML) + +module.exports = SpellcheckDraftStoreExtension diff --git a/internal_packages/composer-spellcheck/spec/fixtures/california-spelling-lookup.json b/internal_packages/composer-spellcheck/spec/fixtures/california-spelling-lookup.json new file mode 100644 index 000000000..a9ad7e79c --- /dev/null +++ b/internal_packages/composer-spellcheck/spec/fixtures/california-spelling-lookup.json @@ -0,0 +1,200 @@ +{ + "1": false, + "5": false, + "9": false, + "13": false, + "14": false, + "15": false, + "16": false, + "37": false, + "58": false, + "1821": false, + "1848": false, + "1850": false, + "34th": false, + "most": false, + "populous": false, + "and": false, + "the": false, + "8th": false, + "or": false, + "9th": false, + "largest": false, + "economy": false, + "in": false, + "world": false, + "If": false, + "it": false, + "were": false, + "a": false, + "country": false, + "California": false, + "would": false, + "be": false, + "California's": false, + "agriculture": false, + "industry": false, + "has": false, + "highest": false, + "output": false, + "of": false, + "any": false, + "U": false, + "S": false, + "State": false, + "Although": false, + "only": false, + "State's": false, + "together": false, + "comprising": false, + "business": false, + "services": false, + "government": false, + "professional": false, + "scientific": false, + "technical": false, + "real": false, + "estate": false, + "finance": false, + "technology": false, + "is": false, + "centered": false, + "on": false, + "About": false, + "000": false, + "earthquakes": false, + "are": false, + "recorded": false, + "each": false, + "year": false, + "but": false, + "too": false, + "small": false, + "to": false, + "felt": false, + "Pacific": false, + "Ring": false, + "Fire": false, + "Earthquakes": false, + "common": false, + "because": false, + "state's": false, + "location": false, + "along": false, + "Florida": false, + "all": false, + "states": false, + "after": false, + "Alaska": false, + "3rd": false, + "longest": false, + "coastline": false, + "contiguous": false, + "United": false, + "States": false, + "Death": false, + "Valley": false, + "lowest": false, + "point": false, + "Mount": false, + "Whitney": false, + "major": false, + "agricultural": false, + "area": false, + "contains": false, + "both": false, + "Central": false, + "areas": false, + "southeast": false, + "The": false, + "center": false, + "state": false, + "dominated": false, + "by": false, + "Mojave": false, + "Desert": false, + "forests": false, + "northwest": false, + "Douglas": false, + "fir": false, + "Redwood": false, + "west": false, + "from": false, + "Coast": false, + "east": false, + "Sierra": false, + "Nevada": false, + "diverse": false, + "geography": false, + "ranges": false, + "starting": false, + "led": false, + "dramati": true, + "dramatic": false, + "sociaal": true, + "social": false, + "demographic": false, + "change": false, + "with": false, + "large-scale": false, + "immigration": false, + "abroad": false, + "an": false, + "accompanying": false, + "economic": false, + "boom": false, + "Gold": false, + "Rush": false, + "western": false, + "portion": false, + "Alta": false, + "was": false, + "organized": false, + "as": false, + "which": false, + "admitted": false, + "31st": false, + "September": false, + "Mexican": false, + "American": false, + "War": false, + "ceded": false, + "war": false, + "for": false, + "independance": true, + "following": false, + "its": false, + "successful": false, + "Mexico": false, + "became": false, + "part": false, + "New": false, + "Spain": false, + "larger": false, + "territory": false, + "Spanish": false, + "Empire": false, + "before": false, + "being": false, + "explored": false, + "number": false, + "European": false, + "expeditions": false, + "during": false, + "16th": false, + "17th": false, + "centuries": false, + "It": false, + "then": false, + "claimed": false, + "various": false, + "Native": false, + "tribes": false, + "What": false, + "now": false, + "first": false, + "setttled": true, + "this": false, + "it's": false, + "doesn't": false +} diff --git a/internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-after.html b/internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-after.html new file mode 100644 index 000000000..75c307f92 --- /dev/null +++ b/internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-after.html @@ -0,0 +1 @@ +

What is now California was first setttled by this it's doesn't various Native American tribes before being explored by a number of European expeditions during the 16th and 17th centuries. It was then claimed by the Spanish Empire as part of Alta California in the larger territory of New Spain. Alta California became a part of Mexico in 1821 following its successful war for independance, but was ceded to the United States in 1848 after the Mexican–American War. The western portion of Alta California was organized as the State of California, which was admitted as the 31st state on September 9, 1850. The California Gold Rush starting in 1848 led to dramati sociaal and demographic change, with large-scale immigration from the east and abroad with an accompanying economic boom.

California's diverse geography ranges from the Sierra Nevada in the east to the Pacific Coast in the west, from the RedwoodDouglas fir forests of the northwest, to the Mojave Desert areas in the southeast. The center of the state is dominated by the Central Valley, a major agricultural area. California contains both the highest point (Mount Whitney) and the lowest point (Death Valley), in the contiguous United States and it has the 3rd longest coastline of all states (after Alaska and Florida). Earthquakes are common because of the state's location along the Pacific Ring of Fire. About 37,000 earthquakes are recorded each year, but most are too small to be felt.[13]

California's economy is centered on technology, finance, real estate services, government, and professional, scientific and technical business services; together comprising 58% of the State economy.[14] Although only 1.5% of the State's economy,[14] California's agriculture industry has the highest output of any U.S. State.[15] If it were a country, California would be the 8th or 9th largest economy in the world[16] and the 34th most populous.

diff --git a/internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-before.html b/internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-before.html new file mode 100644 index 000000000..a95f8d690 --- /dev/null +++ b/internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-before.html @@ -0,0 +1 @@ +

What is now California was first setttled by this it's doesn't various Native American tribes before being explored by a number of European expeditions during the 16th and 17th centuries. It was then claimed by the Spanish Empire as part of Alta California in the larger territory of New Spain. Alta California became a part of Mexico in 1821 following its successful war for independance, but was ceded to the United States in 1848 after the Mexican–American War. The western portion of Alta California was organized as the State of California, which was admitted as the 31st state on September 9, 1850. The California Gold Rush starting in 1848 led to dramati sociaal and demographic change, with large-scale immigration from the east and abroad with an accompanying economic boom.

California's diverse geography ranges from the Sierra Nevada in the east to the Pacific Coast in the west, from the RedwoodDouglas fir forests of the northwest, to the Mojave Desert areas in the southeast. The center of the state is dominated by the Central Valley, a major agricultural area. California contains both the highest point (Mount Whitney) and the lowest point (Death Valley), in the contiguous United States and it has the 3rd longest coastline of all states (after Alaska and Florida). Earthquakes are common because of the state's location along the Pacific Ring of Fire. About 37,000 earthquakes are recorded each year, but most are too small to be felt.[13]

California's economy is centered on technology, finance, real estate services, government, and professional, scientific and technical business services; together comprising 58% of the State economy.[14] Although only 1.5% of the State's economy,[14] California's agriculture industry has the highest output of any U.S. State.[15] If it were a country, California would be the 8th or 9th largest economy in the world[16] and the 34th most populous.

diff --git a/internal_packages/composer/lib/contenteditable-component.cjsx b/internal_packages/composer/lib/contenteditable-component.cjsx index c89f72efd..2eac75315 100644 --- a/internal_packages/composer/lib/contenteditable-component.cjsx +++ b/internal_packages/composer/lib/contenteditable-component.cjsx @@ -97,6 +97,7 @@ class ContenteditableComponent extends React.Component
clipboard.writeText(text) diff --git a/internal_packages/composer/stylesheets/composer.less b/internal_packages/composer/stylesheets/composer.less index b2e308684..584723d47 100644 --- a/internal_packages/composer/stylesheets/composer.less +++ b/internal_packages/composer/stylesheets/composer.less @@ -176,6 +176,12 @@ width: 100%; position: relative; } + spelling.misspelled { + background: linear-gradient(45deg, transparent, transparent 49%, red 49%, transparent 51%); + background-size: 2px 2px; + background-position: bottom; + background-repeat-y: no-repeat; + } } .composer-footer-region { diff --git a/internal_packages/message-list/lib/email-frame.cjsx b/internal_packages/message-list/lib/email-frame.cjsx index d343eacb9..1f6438749 100644 --- a/internal_packages/message-list/lib/email-frame.cjsx +++ b/internal_packages/message-list/lib/email-frame.cjsx @@ -61,7 +61,7 @@ class EmailFrame extends React.Component return unless @_mounted domNode = React.findDOMNode(@) - wrapper = domNode.contentDocument.getElementById("inbox-html-wrapper") + wrapper = domNode.contentDocument.getElementsByTagName('html')[0] height = wrapper.scrollHeight # Why 5px? Some emails have elements with a height of 100%, and then put