//
// This user script adds a preview below textareas and underlines misspelled
// words in red.  Spelling corrections are received from Google using their
// web API.  For this script to work, you need to get a web API key at
//    http://api.google.com/createkey
// You need to create a Google account (you automatically have one if you
// have a Gmail account).
//
// You will see a message about this below all textareas until you install
// a key.
//
// If you make a mistake (like entering an invalid key), go to about:config
// and blank the preference at:
//    greasemonkey.scriptvals.gm/spellcheck.googlekey
// (make sure to "modify" rather than "reset").
//
// CHANGELOG
// 20050806 - fixed to work with GM 0.5
// 20050528 - made it easier to add a web api key
// 20050424 - initial release
//
// TODO
// Store corrections in a local pref variable so you don't make repeat
// requests for the same word across pages/sessions.
//
// This file is licensed under the BSD-new license:
// http://www.opensource.org/licenses/bsd-license.php

// ==UserScript== 
// @name          spellcheck
// @namespace     gm
// @description   Add a spell checker to each textarea.
// @include       http://*
// ==/UserScript==


var key = GM_getValue('googlekey');
var currentTextarea = null;
var updateTimer = null;
var preview = document.createElement('div');
preview.style.border = '1px #222 dashed';

function xpath(expr, doc) {
  if (!doc) {
    doc = document;
  }
  var iter = document.evaluate(expr, doc, null,
                               XPathResult.ANY_TYPE, null);
  var ret = [];
  var n;
  while (n = iter.iterateNext()) {
    ret.push(n);
  }
  return ret;
}

function SpellRequest(spellerObject) {
  this.words = {}; // the set of words being sent
  this.wordList = '';
  this.spell = spellerObject; // pointer to the speller object
}

SpellRequest.prototype.response = function(res) {
  var parser = new DOMParser();
  var dom = parser.parseFromString(res.responseText, "text/xml");
  // check for errors
  var errs = dom.getElementsByTagName('faultstring');
  if (errs.length > 0) {
    for (var e = 0; e < errs.length; e++) {
      GM_log(errs[e].firstChild.nodeValue);
    }
    // prevent further update requests
    speller.updateDictionary = function() {};
    return;
  }
  
  var ret = dom.getElementsByTagName('return');

  var originals, i;
  if (ret.length == 1 && ret[0].firstChild) {
    // got a correction, iterate over the values
    var corrections = ret[0].firstChild.nodeValue.split(',');
    originals = this.wordList.split(',');
    // assert that corrections.length == originals.length
    for (i = 0; i < originals.length; i++) {
      if (originals[i] != corrections[i]) {
        this.spell.misspelled[originals[i]] = corrections[i];
        this.spell.dictionary[corrections[i]] = true;
      }
      this.spell.dictionary[originals[i]] = true;
    }
    this.spell.updatepreview();
  } else { // words are correct
    originals = this.wordList.split(',');
    for (i = 0; i < originals.length; i++)
      this.spell.dictionary[originals[i]] = true;
  }
};
    
SpellRequest.prototype.send = function() {
  var list = [];
  for (var w in this.words)
    list.push(w);
  
  if (list.length == 0) // nothing to send, abort
    return;
    
  this.wordList = list.join(',');

  // make a closure
  var key = GM_getValue('googlekey');
  if (!key)
    return;
  
  var self = this;
  GM_xmlhttpRequest({
    method: 'POST',
    url: 'http://api.google.com/search/beta2',
    onload: function(res) {
      self.response(res);
    },
    headers: {'Content-Type': 'text/xml' },
    data: '<?xml version="1.0" encoding="UTF-8"?>\n\n'
        + '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:xsd="http://www.w3.org/1999/XMLSchema">\n'
        + '<SOAP-ENV:Body>\n'
        + '  <ns1:doSpellingSuggestion xmlns:ns1="urn:GoogleSearch" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\n'
        + '    <key xsi:type="xsd:string">' + key + '</key>\n'
        + '    <phrase xsi:type="xsd:string">' + this.wordList + '</phrase>\n'
        + '  </ns1:doSpellingSuggestion>\n'
        + '</SOAP-ENV:Body>\n'
        + '</SOAP-ENV:Envelope>\n'
    });
}

SpellRequest.prototype.addWord = function(w) {
  this.words[w] = true;
}

function MapToTextNodes(root, f) {
  var len = root.childNodes.length;
  for (var c = 0; c < len; ++c) { // walk the dom
    MapToTextNodes(root.childNodes[c], f);
  }
  
  // 3 is a  text node
  if (3 == root.nodeType) {
    f(root);
  }
}

// supports for multiple text areas and outputs would be nice (with shared dictionary)
var speller = {
  dictionary: {}, // seen words
  newWords: {}, // unseen words
  misspelled: {}, // words that are known to be misspelled
  lastUpdate: 0,
  updateInterval: 500,

  keyupdate: function(ev) {
    this.updatepreview();
  },
  
  updatepreview: function() {
    // updatepreview is an expensive operation.  we try to rate limit it.
    var st = (new Date()).getTime();
    if (st - this.lastUpdate < this.updateInterval)
      return;
    
    this.lastUpdate = st;
    
    var text = currentTextarea.value;
    text = text.replace(/\n/g,"<br />\n");

    // now process the tree
    var hidden = document.createElement('div');
    hidden.innerHTML = text;
    this.findAndReplace(hidden);
    
    // preview is a global variable
    preview.innerHTML = hidden.innerHTML;

    
    /*  // for debugging
    //var raw = document.getElementById('raw');
    preview = document.getElementById('preview');
    var tmp = preview.innerHTML.replace(/</g, "&lt;");
    raw.innerHTML = tmp.replace(/>/g, "&gt;");
    //*/
    
    var et = (new Date()).getTime();
    this.updateInterval = Math.max((et-st) * 2, 200);
  },
  
  findAndReplace: function(root) {
    var self = this;
    MapToTextNodes(root, function(node) {
        var words = node.nodeValue.split(/(\W+)/);
        for (var w = 0; w < words.length; w++) {
          var correction = self.misspelled[words[w].toLowerCase()];
          if (correction)
            words[w] = "<span style='border-bottom: 2px #f00 solid' title='"
                       + correction + "'>"
                       + words[w] + "</span>";
        }
        
        var newNode = document.createElement('span');
        newNode.innerHTML = words.join('');
        node.parentNode.replaceChild(newNode, node);
      });
  },

  updateDictionary: function() {
    // determine what words are new and send a request

    var text = currentTextarea.value;
    var elt = document.createElement('div');
    elt.innerHTML = text;

    var req = new SpellRequest(this);
    var self = this;
    MapToTextNodes(elt, function(node) {
        var newWords = node.nodeValue.split(/\W+/);
        var len = newWords.length;
        for (var i = 0; i < len; i++) {
          var word = newWords[i].toLowerCase();
          if (!self.dictionary[word] && word.length > 1)
            req.addWord(word);
        }
      });
    
    req.send();
  }
}

function insertAfter(newNode, existingNode) {
  var sib = existingNode.nextSibling;
  if (sib) {
    existingNode.parentNode.insertBefore(newNode, sib);
  } else {
    existingNode.parentNode.appendChild(newNode);
  }
}

function textareaFocus(ev) {
  currentTextarea = this;
  var compStyle = document.defaultView.getComputedStyle(this, '');
  preview.style.width = compStyle.width;
  insertAfter(preview, this);
  speller.updatepreview();
}
function textareaKeyUp(ev) {
  if (this == currentTextarea) {
    speller.updatepreview();
  }
}

function installKey(ev) {
  key = this.previousSibling.value;
  GM_setValue('googlekey', key);
  window.alert('Key installed.  Reload page and select a textarea to use it.');
}

window.addEventListener('load', function() {
    initDict(speller.dictionary);
    // look for first textarea
    var textareas = document.getElementsByTagName('textarea');
    var i;
    if (textareas.length > 0) {
      if (key) {
        for (i = 0; i < textareas.length; i++) {
          textareas[i].addEventListener('focus', textareaFocus, false);
          textareas[i].addEventListener('keyup', textareaKeyUp, false);
        }
        currentTextarea = textareas[0];
        window.setInterval(function() { speller.updateDictionary() }, 2000);
      } else {
        // no google api key
        preview.innerHTML = '<p>To use the greasemonkey spell check '
          + 'script, you need to get a <a href="http://api.google.com/'
          + 'createkey">Google web API</a> and then install the key.</p>'
          + '<p><input /><input type="button" class="_gm_sc_key" value='
          + '"install key" /></p>';
        for (i = 0; i < textareas.length; ++i) {
          var copy = preview.cloneNode(true);
          var compStyle = document.defaultView
                                  .getComputedStyle(textareas[i], '');
          copy.style.width = compStyle.width;
          insertAfter(copy, textareas[i]);
        }
        
        // add the event handler to the "install the key" instances
        var inputs = xpath('//input[@class="_gm_sc_key"]', document);
        for (i = 0; i < inputs.length; ++i) {
          inputs[i].addEventListener('click', installKey, true);
        }
      }
    }
  }, false);

/**
 * Prepopulate the dictionary with a list of the 300 most common English
 * (US) words.
 */
function initDict(dict) {
  dict['the'] = true;
  dict['of'] = true;
  dict['and'] = true;
  dict['to'] = true;
  dict['in'] = true;
  dict['is'] = true;
  dict['you'] = true;
  dict['that'] = true;
  dict['it'] = true;
  dict['he'] = true;
  dict['for'] = true;
  dict['was'] = true;
  dict['on'] = true;
  dict['are'] = true;
  dict['as'] = true;
  dict['with'] = true;
  dict['his'] = true;
  dict['they'] = true;
  dict['at'] = true;
  dict['be'] = true;
  dict['this'] = true;
  dict['from'] = true;
  dict['have'] = true;
  dict['or'] = true;
  dict['by'] = true;
  dict['one'] = true;
  dict['had'] = true;
  dict['not'] = true;
  dict['but'] = true;
  dict['what'] = true;
  dict['all'] = true;
  dict['were'] = true;
  dict['when'] = true;
  dict['we'] = true;
  dict['there'] = true;
  dict['can'] = true;
  dict['an'] = true;
  dict['your'] = true;
  dict['which'] = true;
  dict['their'] = true;
  dict['said'] = true;
  dict['if'] = true;
  dict['do'] = true;
  dict['will'] = true;
  dict['each'] = true;
  dict['about'] = true;
  dict['how'] = true;
  dict['up'] = true;
  dict['out'] = true;
  dict['them'] = true;
  dict['then'] = true;
  dict['she'] = true;
  dict['many'] = true;
  dict['some'] = true;
  dict['so'] = true;
  dict['these'] = true;
  dict['would'] = true;
  dict['other'] = true;
  dict['into'] = true;
  dict['has'] = true;
  dict['more'] = true;
  dict['her'] = true;
  dict['two'] = true;
  dict['like'] = true;
  dict['him'] = true;
  dict['see'] = true;
  dict['time'] = true;
  dict['could'] = true;
  dict['no'] = true;
  dict['make'] = true;
  dict['than'] = true;
  dict['first'] = true;
  dict['been'] = true;
  dict['its'] = true;
  dict['who'] = true;
  dict['now'] = true;
  dict['people'] = true;
  dict['my'] = true;
  dict['made'] = true;
  dict['over'] = true;
  dict['did'] = true;
  dict['down'] = true;
  dict['only'] = true;
  dict['way'] = true;
  dict['find'] = true;
  dict['use'] = true;
  dict['may'] = true;
  dict['water'] = true;
  dict['long'] = true;
  dict['little'] = true;
  dict['very'] = true;
  dict['after'] = true;
  dict['words'] = true;
  dict['called'] = true;
  dict['just'] = true;
  dict['where'] = true;
  dict['most'] = true;
  dict['know'] = true;
  dict['get'] = true;
  dict['through'] = true;
  dict['back'] = true;
  dict['much'] = true;
  dict['before'] = true;
  dict['go'] = true;
  dict['good'] = true;
  dict['new'] = true;
  dict['write'] = true;
  dict['our'] = true;
  dict['used'] = true;
  dict['me'] = true;
  dict['man'] = true;
  dict['too'] = true;
  dict['any'] = true;
  dict['day'] = true;
  dict['same'] = true;
  dict['right'] = true;
  dict['look'] = true;
  dict['think'] = true;
  dict['also'] = true;
  dict['around'] = true;
  dict['another'] = true;
  dict['came'] = true;
  dict['come'] = true;
  dict['work'] = true;
  dict['three'] = true;
  dict['word'] = true;
  dict['must'] = true;
  dict['because'] = true;
  dict['does'] = true;
  dict['part'] = true;
  dict['even'] = true;
  dict['place'] = true;
  dict['well'] = true;
  dict['such'] = true;
  dict['here'] = true;
  dict['take'] = true;
  dict['why'] = true;
  dict['things'] = true;
  dict['help'] = true;
  dict['put'] = true;
  dict['years'] = true;
  dict['different'] = true;
  dict['away'] = true;
  dict['again'] = true;
  dict['off'] = true;
  dict['went'] = true;
  dict['old'] = true;
  dict['number'] = true;
  dict['great'] = true;
  dict['tell'] = true;
  dict['men'] = true;
  dict['say'] = true;
  dict['small'] = true;
  dict['every'] = true;
  dict['found'] = true;
  dict['still'] = true;
  dict['between'] = true;
  dict['name'] = true;
  dict['should'] = true;
  dict['Mr'] = true;
  dict['home'] = true;
  dict['big'] = true;
  dict['give'] = true;
  dict['air'] = true;
  dict['line'] = true;
  dict['set'] = true;
  dict['own'] = true;
  dict['under'] = true;
  dict['read'] = true;
  dict['last'] = true;
  dict['never'] = true;
  dict['us'] = true;
  dict['left'] = true;
  dict['end'] = true;
  dict['along'] = true;
  dict['while'] = true;
  dict['might'] = true;
  dict['next'] = true;
  dict['sound'] = true;
  dict['below'] = true;
  dict['saw'] = true;
  dict['something'] = true;
  dict['thought'] = true;
  dict['both'] = true;
  dict['few'] = true;
  dict['those'] = true;
  dict['always'] = true;
  dict['looked'] = true;
  dict['show'] = true;
  dict['large'] = true;
  dict['often'] = true;
  dict['together'] = true;
  dict['asked'] = true;
  dict['house'] = true;
  dict["don't"] = true;
  dict['world'] = true;
  dict['going'] = true;
  dict['want'] = true;
  dict['school'] = true;
  dict['important'] = true;
  dict['until'] = true;
  dict['1'] = true;
  dict['form'] = true;
  dict['food'] = true;
  dict['keep'] = true;
  dict['children'] = true;
  dict['feet'] = true;
  dict['land'] = true;
  dict['side'] = true;
  dict['without'] = true;
  dict['boy'] = true;
  dict['once'] = true;
  dict['animals'] = true;
  dict['life'] = true;
  dict['enough'] = true;
  dict['took'] = true;
  dict['sometimes'] = true;
  dict['four'] = true;
  dict['head'] = true;
  dict['above'] = true;
  dict['kind'] = true;
  dict['began'] = true;
  dict['almost'] = true;
  dict['live'] = true;
  dict['page'] = true;
  dict['got'] = true;
  dict['earth'] = true;
  dict['need'] = true;
  dict['far'] = true;
  dict['hand'] = true;
  dict['high'] = true;
  dict['year'] = true;
  dict['mother'] = true;
  dict['light'] = true;
  dict['parts'] = true;
  dict['country'] = true;
  dict['father'] = true;
  dict['let'] = true;
  dict['night'] = true;
  dict['following'] = true;
  dict['2'] = true;
  dict['picture'] = true;
  dict['being'] = true;
  dict['study'] = true;
  dict['second'] = true;
  dict['eyes'] = true;
  dict['soon'] = true;
  dict['times'] = true;
  dict['story'] = true;
  dict['boys'] = true;
  dict['since'] = true;
  dict['white'] = true;
  dict['days'] = true;
  dict['ever'] = true;
  dict['paper'] = true;
  dict['hard'] = true;
  dict['near'] = true;
  dict['sentence'] = true;
  dict['better'] = true;
  dict['best'] = true;
  dict['across'] = true;
  dict['during'] = true;
  dict['today'] = true;
  dict['others'] = true;
  dict['however'] = true;
  dict['sure'] = true;
  dict['means'] = true;
  dict['knew'] = true;
  dict["it's"] = true;
  dict['try'] = true;
  dict['told'] = true;
  dict['young'] = true;
  dict['miles'] = true;
  dict['sun'] = true;
  dict['ways'] = true;
  dict['thing'] = true;
  dict['whole'] = true;
  dict['hear'] = true;
  dict['example'] = true;
  dict['heard'] = true;
  dict['several'] = true;
  dict['change'] = true;
  dict['answer'] = true;
  dict['room'] = true;
  dict['sea'] = true;
  dict['against'] = true;
  dict['top'] = true;
  dict['turned'] = true;
  dict['learn'] = true;
  dict['point'] = true;
  dict['city'] = true;
  dict['play'] = true;
  dict['toward'] = true;
  dict['five'] = true;
  dict['using'] = true;
  dict['himself'] = true;
  dict['usually'] = true;
}

