2015-12-10 07:10:55 +08:00
|
|
|
|
{ComposerExtension, AccountStore, DOMUtils, NylasSpellchecker} = require 'nylas-exports'
|
2015-09-03 04:20:01 +08:00
|
|
|
|
_ = require 'underscore'
|
2015-11-19 04:09:07 +08:00
|
|
|
|
remote = require('remote')
|
|
|
|
|
MenuItem = remote.require('menu-item')
|
2015-12-10 07:10:55 +08:00
|
|
|
|
spellchecker = NylasSpellchecker
|
2015-09-03 04:20:01 +08:00
|
|
|
|
|
|
|
|
|
SpellcheckCache = {}
|
|
|
|
|
|
2015-11-28 03:49:24 +08:00
|
|
|
|
class SpellcheckComposerExtension extends ComposerExtension
|
2015-09-03 04:20:01 +08:00
|
|
|
|
|
|
|
|
|
@isMisspelled: (word) ->
|
2015-11-19 04:09:07 +08:00
|
|
|
|
SpellcheckCache[word] ?= spellchecker.isMisspelled(word)
|
2015-09-03 04:20:01 +08:00
|
|
|
|
SpellcheckCache[word]
|
|
|
|
|
|
2015-12-31 00:36:47 +08:00
|
|
|
|
@onContentChanged: ({editor}) =>
|
2016-01-12 09:31:03 +08:00
|
|
|
|
@walkTree(editor)
|
2015-09-03 04:20:01 +08:00
|
|
|
|
|
2015-12-31 00:36:47 +08:00
|
|
|
|
@onShowContextMenu: ({editor, event, menu}) =>
|
2015-12-22 11:58:01 +08:00
|
|
|
|
selection = editor.currentSelection()
|
2015-11-19 04:09:07 +08:00
|
|
|
|
range = DOMUtils.Mutating.getRangeAtAndSelectWord(selection, 0)
|
|
|
|
|
word = range.toString()
|
|
|
|
|
if @isMisspelled(word)
|
|
|
|
|
corrections = spellchecker.getCorrectionsForMisspelling(word)
|
|
|
|
|
if corrections.length > 0
|
|
|
|
|
corrections.forEach (correction) =>
|
|
|
|
|
menu.append(new MenuItem({
|
|
|
|
|
label: correction,
|
2016-01-12 09:31:03 +08:00
|
|
|
|
click: @applyCorrection.bind(@, editor, range, selection, correction)
|
2015-11-19 04:09:07 +08:00
|
|
|
|
}))
|
|
|
|
|
else
|
|
|
|
|
menu.append(new MenuItem({ label: 'No Guesses Found', enabled: false}))
|
|
|
|
|
|
|
|
|
|
menu.append(new MenuItem({ type: 'separator' }))
|
|
|
|
|
menu.append(new MenuItem({
|
|
|
|
|
label: 'Learn Spelling',
|
2016-01-12 09:31:03 +08:00
|
|
|
|
click: @learnSpelling.bind(@, editor, word)
|
2015-11-19 04:09:07 +08:00
|
|
|
|
}))
|
|
|
|
|
menu.append(new MenuItem({ type: 'separator' }))
|
|
|
|
|
|
2016-01-12 09:31:03 +08:00
|
|
|
|
@applyCorrection: (editor, range, selection, correction) =>
|
2015-11-19 04:09:07 +08:00
|
|
|
|
DOMUtils.Mutating.applyTextInRange(range, selection, correction)
|
2016-01-12 09:31:03 +08:00
|
|
|
|
@walkTree(editor)
|
2015-11-19 04:09:07 +08:00
|
|
|
|
|
2016-01-12 09:31:03 +08:00
|
|
|
|
@learnSpelling: (editor, word) =>
|
2015-11-19 04:09:07 +08:00
|
|
|
|
spellchecker.add(word)
|
2015-09-09 08:52:26 +08:00
|
|
|
|
delete SpellcheckCache[word]
|
2016-01-12 09:31:03 +08:00
|
|
|
|
@walkTree(editor)
|
2015-09-03 04:20:01 +08:00
|
|
|
|
|
2016-01-12 09:31:03 +08:00
|
|
|
|
@walkTree: (editor) =>
|
|
|
|
|
# Remove all existing spellcheck nodes
|
|
|
|
|
spellingNodes = editor.rootNode.querySelectorAll('spelling')
|
|
|
|
|
for node in spellingNodes
|
|
|
|
|
editor.whilePreservingSelection =>
|
|
|
|
|
DOMUtils.unwrapNode(node)
|
|
|
|
|
|
|
|
|
|
# Normalize to make sure words aren't split across text nodes
|
|
|
|
|
editor.rootNode.normalize()
|
2015-09-03 04:20:01 +08:00
|
|
|
|
|
|
|
|
|
selection = document.getSelection()
|
|
|
|
|
selectionSnapshot =
|
|
|
|
|
anchorNode: selection.anchorNode
|
|
|
|
|
anchorOffset: selection.anchorOffset
|
|
|
|
|
focusNode: selection.focusNode
|
|
|
|
|
focusOffset: selection.focusOffset
|
|
|
|
|
selectionImpacted = false
|
|
|
|
|
|
2016-01-12 09:31:03 +08:00
|
|
|
|
treeWalker = document.createTreeWalker(editor.rootNode, NodeFilter.SHOW_TEXT)
|
|
|
|
|
nodeList = []
|
|
|
|
|
nodeMisspellingsFound = 0
|
|
|
|
|
|
2015-09-03 04:20:01 +08:00
|
|
|
|
while (treeWalker.nextNode())
|
|
|
|
|
nodeList.push(treeWalker.currentNode)
|
|
|
|
|
|
2016-01-12 09:31:03 +08:00
|
|
|
|
# Note: As a performance optimization, we stop spellchecking after encountering
|
|
|
|
|
# 10 misspelled words. This keeps the runtime of this method bounded!
|
|
|
|
|
|
|
|
|
|
while (node = nodeList.shift())
|
|
|
|
|
break if nodeMisspellingsFound > 10
|
2015-09-03 04:20:01 +08:00
|
|
|
|
str = node.textContent
|
|
|
|
|
|
|
|
|
|
# https://regex101.com/r/bG5yC4/1
|
|
|
|
|
wordRegexp = /(\w[\w'’-]*\w|\w)/g
|
|
|
|
|
|
|
|
|
|
while ((match = wordRegexp.exec(str)) isnt null)
|
2016-01-12 09:31:03 +08:00
|
|
|
|
break if nodeMisspellingsFound > 10
|
2015-09-03 04:20:01 +08:00
|
|
|
|
misspelled = @isMisspelled(match[0])
|
|
|
|
|
|
2016-01-12 09:31:03 +08:00
|
|
|
|
if misspelled
|
2015-09-12 02:12:25 +08:00
|
|
|
|
# The insertion point is currently at the end of this misspelled word.
|
|
|
|
|
# Do not mark it until the user types a space or leaves.
|
|
|
|
|
if selectionSnapshot.focusNode is node and selectionSnapshot.focusOffset is match.index + match[0].length
|
|
|
|
|
continue
|
2015-10-23 07:08:03 +08:00
|
|
|
|
|
2016-01-12 09:31:03 +08:00
|
|
|
|
if match.index is 0
|
|
|
|
|
matchNode = node
|
2015-09-03 04:20:01 +08:00
|
|
|
|
else
|
2016-01-12 09:31:03 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
nodeMisspellingsFound += 1
|
|
|
|
|
nodeList.unshift(afterMatchNode)
|
|
|
|
|
break
|
2015-09-03 04:20:01 +08:00
|
|
|
|
|
|
|
|
|
if selectionImpacted
|
|
|
|
|
selection.setBaseAndExtent(selectionSnapshot.anchorNode, selectionSnapshot.anchorOffset, selectionSnapshot.focusNode, selectionSnapshot.focusOffset)
|
|
|
|
|
|
2015-12-31 00:36:47 +08:00
|
|
|
|
@finalizeSessionBeforeSending: ({session}) ->
|
2015-09-03 04:20:01 +08:00
|
|
|
|
body = session.draft().body
|
|
|
|
|
clean = body.replace(/<\/?spelling[^>]*>/g, '')
|
|
|
|
|
if body != clean
|
2015-12-24 09:25:30 +08:00
|
|
|
|
return session.changes.add(body: clean)
|
2015-09-03 04:20:01 +08:00
|
|
|
|
|
2015-11-28 03:49:24 +08:00
|
|
|
|
SpellcheckComposerExtension.SpellcheckCache = SpellcheckCache
|
2015-09-03 04:20:01 +08:00
|
|
|
|
|
2015-11-28 03:49:24 +08:00
|
|
|
|
module.exports = SpellcheckComposerExtension
|