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
This commit is contained in:
Ben Gotow 2015-09-02 13:20:01 -07:00
parent 62fbd61ede
commit 502bb7c0b8
10 changed files with 379 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -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": {
}
}

View file

@ -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

View file

@ -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
}

View file

@ -0,0 +1 @@
<p>What is now California was first <spelling class="misspelled">setttled</spelling> by this it's doesn't <a href="/wiki/Indigenous_peoples_of_California" title="Indigenous peoples of California">various Native American tribes</a> before being explored by a number of European expeditions during the 16th and 17th centuries. It was then claimed by the <a href="/wiki/Spanish_Empire" title="Spanish Empire">Spanish Empire</a> as part of <a href="/wiki/Alta_California" title="Alta California">Alta California</a> in the larger territory of <a href="/wiki/New_Spain" title="New Spain">New Spain</a>. Alta California became a part of <a href="/wiki/Mexico" title="Mexico">Mexico</a> in 1821 following its successful <a href="/wiki/Mexican_War_of_Independence" title="Mexican War of Independence">war for <spelling class="misspelled">independance</spelling></a>, but was ceded to the <a href="/wiki/United_States" title="United States">United States</a> in 1848 after the <a href="/wiki/Mexican%E2%80%93American_War" title="MexicanAmerican War">MexicanAmerican War</a>. 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 <a href="/wiki/California_Gold_Rush" title="California Gold Rush">California Gold Rush</a> starting in 1848 led to <spelling class="misspelled">dramati</spelling> <spelling class="misspelled">sociaal</spelling> and demographic change, with large-scale immigration from the east and abroad with an accompanying economic boom.</p><p>California's diverse geography ranges from the <a href="/wiki/Sierra_Nevada_(U.S.)" title="Sierra Nevada (U.S.)">Sierra Nevada</a> in the east to the <a href="/wiki/West_Coast_of_the_United_States" title="West Coast of the United States">Pacific Coast</a> in the west, from the <a href="/wiki/Sequoia_sempervirens" title="Sequoia sempervirens">Redwood</a><a href="/wiki/Douglas_fir" title="Douglas fir">Douglas fir</a> forests of the northwest, to the <a href="/wiki/Mojave_Desert" title="Mojave Desert">Mojave Desert</a> areas in the southeast. The center of the state is dominated by the <a href="/wiki/California_Central_Valley" title="California Central Valley" class="mw-redirect">Central Valley</a>, a major agricultural area. California contains both the highest point (<a href="/wiki/Mount_Whitney" title="Mount Whitney">Mount Whitney</a>) and the lowest point (<a href="/wiki/Death_Valley" title="Death Valley">Death Valley</a>), in the <a href="/wiki/Contiguous_United_States" title="Contiguous United States">contiguous United States</a> and it has the <a href="/wiki/List_of_U.S._states_by_coastline" title="List of U.S. states by coastline">3rd longest coastline</a> of all states (after Alaska and <a href="/wiki/Florida" title="Florida">Florida</a>). Earthquakes are common because of the state's location along the <a href="/wiki/Pacific_Ring_of_Fire" title="Pacific Ring of Fire" class="mw-redirect">Pacific Ring of Fire</a>. About 37,000 earthquakes are recorded each year, but most are too small to be felt.<sup id="cite_ref-13" class="reference"><a href="#cite_note-13"><span>[</span>13<span>]</span></a></sup></p><p>California's economy is centered on <a href="/wiki/Technology" title="Technology">technology</a>, <a href="/wiki/Finance" title="Finance">finance</a>, <a href="/wiki/Real_estate" title="Real estate">real estate services</a>, government, and professional, scientific and technical <a href="/wiki/Business_services" title="Businaaaaasess services" class="mw-redirect">business services</a>; together comprising 58% of the State economy.<sup id="cite_ref-BEA_14-0" class="reference"><a href="#cite_note-BEA-14"><span>[</span>14<span>]</span></a></sup> Although only 1.5% of the State's economy,<sup id="cite_ref-BEA_14-1" class="reference"><a href="#cite_note-BEA-14"><span>[</span>14<span>]</span></a></sup> California's agriculture industry has the highest output of any U.S. State.<sup id="cite_ref-15" class="reference"><a href="#cite_note-15"><span>[</span>15<span>]</span></a></sup> If it were a country, California would be the <a href="/wiki/Comparison_between_U.S._states_and_countries_nominal_GDP" title="Comparison between U.S. states and countries nominal GDP" class="mw-redirect">8th or 9th largest economy in the world</a><sup id="cite_ref-16" class="reference"><a href="#cite_note-16"><span>[</span>16<span>]</span></a></sup> and the <a href="/wiki/List_of_countries_by_population" title="List of countries by population" class="mw-redirect">34th most populous</a>.</p>

View file

@ -0,0 +1 @@
<p>What is now California was first setttled by this it's doesn't <a href="/wiki/Indigenous_peoples_of_California" title="Indigenous peoples of California">various Native American tribes</a> before being explored by a number of European expeditions during the 16th and 17th centuries. It was then claimed by the <a href="/wiki/Spanish_Empire" title="Spanish Empire">Spanish Empire</a> as part of <a href="/wiki/Alta_California" title="Alta California">Alta California</a> in the larger territory of <a href="/wiki/New_Spain" title="New Spain">New Spain</a>. Alta California became a part of <a href="/wiki/Mexico" title="Mexico">Mexico</a> in 1821 following its successful <a href="/wiki/Mexican_War_of_Independence" title="Mexican War of Independence">war for independance</a>, but was ceded to the <a href="/wiki/United_States" title="United States">United States</a> in 1848 after the <a href="/wiki/Mexican%E2%80%93American_War" title="MexicanAmerican War">MexicanAmerican War</a>. 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 <a href="/wiki/California_Gold_Rush" title="California Gold Rush">California Gold Rush</a> starting in 1848 led to dramati sociaal and demographic change, with large-scale immigration from the east and abroad with an accompanying economic boom.</p><p>California's diverse geography ranges from the <a href="/wiki/Sierra_Nevada_(U.S.)" title="Sierra Nevada (U.S.)">Sierra Nevada</a> in the east to the <a href="/wiki/West_Coast_of_the_United_States" title="West Coast of the United States">Pacific Coast</a> in the west, from the <a href="/wiki/Sequoia_sempervirens" title="Sequoia sempervirens">Redwood</a><a href="/wiki/Douglas_fir" title="Douglas fir">Douglas fir</a> forests of the northwest, to the <a href="/wiki/Mojave_Desert" title="Mojave Desert">Mojave Desert</a> areas in the southeast. The center of the state is dominated by the <a href="/wiki/California_Central_Valley" title="California Central Valley" class="mw-redirect">Central Valley</a>, a major agricultural area. California contains both the highest point (<a href="/wiki/Mount_Whitney" title="Mount Whitney">Mount Whitney</a>) and the lowest point (<a href="/wiki/Death_Valley" title="Death Valley">Death Valley</a>), in the <a href="/wiki/Contiguous_United_States" title="Contiguous United States">contiguous United States</a> and it has the <a href="/wiki/List_of_U.S._states_by_coastline" title="List of U.S. states by coastline">3rd longest coastline</a> of all states (after Alaska and <a href="/wiki/Florida" title="Florida">Florida</a>). Earthquakes are common because of the state's location along the <a href="/wiki/Pacific_Ring_of_Fire" title="Pacific Ring of Fire" class="mw-redirect">Pacific Ring of Fire</a>. About 37,000 earthquakes are recorded each year, but most are too small to be felt.<sup id="cite_ref-13" class="reference"><a href="#cite_note-13"><span>[</span>13<span>]</span></a></sup></p><p>California's economy is centered on <a href="/wiki/Technology" title="Technology">technology</a>, <a href="/wiki/Finance" title="Finance">finance</a>, <a href="/wiki/Real_estate" title="Real estate">real estate services</a>, government, and professional, scientific and technical <a href="/wiki/Business_services" title="Businaaaaasess services" class="mw-redirect">business services</a>; together comprising 58% of the State economy.<sup id="cite_ref-BEA_14-0" class="reference"><a href="#cite_note-BEA-14"><span>[</span>14<span>]</span></a></sup> Although only 1.5% of the State's economy,<sup id="cite_ref-BEA_14-1" class="reference"><a href="#cite_note-BEA-14"><span>[</span>14<span>]</span></a></sup> California's agriculture industry has the highest output of any U.S. State.<sup id="cite_ref-15" class="reference"><a href="#cite_note-15"><span>[</span>15<span>]</span></a></sup> If it were a country, California would be the <a href="/wiki/Comparison_between_U.S._states_and_countries_nominal_GDP" title="Comparison between U.S. states and countries nominal GDP" class="mw-redirect">8th or 9th largest economy in the world</a><sup id="cite_ref-16" class="reference"><a href="#cite_note-16"><span>[</span>16<span>]</span></a></sup> and the <a href="/wiki/List_of_countries_by_population" title="List of countries by population" class="mw-redirect">34th most populous</a>.</p>

View file

@ -97,6 +97,7 @@ class ContenteditableComponent extends React.Component
<div id="contenteditable"
ref="contenteditable"
contentEditable
spellCheck={false}
tabIndex={@props.tabIndex}
style={@props.style ? {}}
onBlur={@_onBlur}
@ -633,6 +634,9 @@ class ContenteditableComponent extends React.Component
range.selectNode(node)
selection.removeAllRanges()
selection.addRange(range)
for extension in DraftStore.extensions()
if extension.onSubstitutionPerformed
extension.onSubstitutionPerformed(@_editableNode())
cut = =>
clipboard.writeText(text)

View file

@ -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 {

View file

@ -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