Browsed slashdot earlier today, and saw a post about SpellingCow, a remotely hosted ajax spell checker.
My initial reaction was “cool, someone picked up on my ajax spell checker idea and made something useful out of it”.
However as soon as I tried it I noticed a remarkable resemblance to the LiteSpellChecker component I wrote, as a proof of concept, about a year ago.
LiteSpellChecker

SpellingCow

That menu looks a bit too similar for it to be a coincidence so I decided to dig a bit deeper.
Perhaps they’d actually based it on my implementation and fixed some of the bugs in it, that would have been nice. As both my spell checker implementations are available under the MIT license there’s nothing preventing anyone from using my code as a base or starting point.
Their javascript file starts out with the following comment and all code is obfuscated.
/*
* © 2004-2006 SpellingCow Software - All rights reserved. This is not free software.
*/
Not satisfied with that I started to de-obfuscate the code. Quite soon I encountered segments that looked awfully familiar:
Style declaration
webfx-spellchecker-menu {
border: 1px solid;
border-color: threedlightshadow threeddarkshadow threeddarkshadow threedlightshadow;
position: absolute;
}
.webfx-spellchecker-menu .inner {
border: 1px solid;
border-color: threedhighlight threedshadow threedshadow threedhighlight;
background: threedface;
padding: 2px;
}
.webfx-spellchecker-menu a {
display: block;
font: menu;
color: menutext;
padding: 1px 5ex 1px 3ex;
text-decoration: none;
}
.webfx-spellchecker-menu a:hover {
background: highlight;
color: highlighttext;
}
.webfx-spellchecker-menu .separator {
border-top: 1px solid threedshadow;
border-bottom: 1px solid threedhighlight;
overflow: hidden;
margin: 2px;
}
webFXSpellCheckHandler.invalidWordBg =
'url(http://me.eae.net/stuff/spellchecker/images/redline.png) repeat-x bottom';
this.g('.sc_ayt_menu','
border: 1px solid ;
border-color: threedlightshadow threeddarkshadow threeddarkshadow threedlightshadow;
padding:0px;
position:absolute;
z-index:4;
');
this.g('.sc_ayt_menu .inner','
border: 1px solid ;
border-color: threedhighlight threedshadow threedshadow threedhighlight;
background: threedface;
padding: 2px;
text-align:left
');
this.g('.sc_ayt_menu a','
display: block;
font: menu;
color: menutext;
padding: 1px 5ex 1px 3ex;
text-decoration: none;
');
this.g('.sc_ayt_menu a:hover','
background: highlight;
color: highlighttext;
');
this.g('.sc_ayt_menu .separator','
border-top: 1px solid threedshadow;
border-bottom: 1px solid threedhighlight;
overflow: hidden;
margin: 2px;
padding: 0px;
');
this.g('#spellingcow_div .red_span',
az + "background: url(" + ayty + "images/redline.png) repeat-x bottom;
");
The declaration for the menu is pretty much exactly the same and even though SpellingCow marks misspelled words with a yellow background there’s still a style declaration using reline.png, precisely matching the one I use.
Menu Creation
webFXSpellCheckHandler._init = function() {
var menu, inner, item;
menu = document.createElement('div');
menu.id = 'webfxSpellCheckMenu';
menu.className = 'webfx-spellchecker-menu';
menu.style.display = 'none';
inner = document.createElement('div');
inner.className = 'inner';
menu.appendChild(inner);
item = document.createElement('div');
item.className = 'separator';
inner.appendChild(item);
item = document.createElement('a');
item.href = 'javascript:webFXSpellCheckHandler._ignoreWord();'
item.appendChild(document.createTextNode('Ignore'));
inner.appendChild(item);
document.body.appendChild(menu);
};
function aytv(){
this.ad = document.createElement('div');
this.ad.className = 'sc_ayt_menu';
this.ad.style.display = 'none';
var et = document.createElement('div');
et.className = 'inner';
this.ad.appendChild(et);
var bb = document.createElement('div');
bb.className = 'separator';
et.appendChild(bb);
bb = document.createElement('a');
if (aytaa) { bb.href = 'javascript:aytk.ignore_word();' }
else { bb.href = 'javascript:oSpell.ignore_word();' }
bb.appendChild(document.createTextNode('Ignore'));
et.appendChild(bb);
}
Code for constructing the popup menu on initilization. The SpellingCow implementation does it exatly the same way as mine, it even uses the same class and method names.
Displaying suggestions
webFXSpellCheckHandler._showSuggestionsMenu = function(e, el, word, instance) {
var menu, len, item, sep, frame, aSuggestions, doc, x, y, o;
if (!webFXSpellCheckHandler.words[word]) { return; }
menu = document.getElementById('webfxSpellCheckMenu');
len = menu.firstChild.childNodes.length;
while (len > 2) { menu.firstChild.removeChild(menu.firstChild.firstChild); len--; }
sep = menu.firstChild.firstChild;
aSuggestions = webFXSpellCheckHandler.words[word][1];
len = aSuggestions.length;
if (len > 10) { len = 10; }
for (i = 0; i < len; i++) {
item = document.createElement('a');
item.href = 'javascript:webFXSpellCheckHandler._replaceWord(' + instance + ', "' + aSuggestions[i] + '");'
item.appendChild(document.createTextNode(aSuggestions[i]));
menu.firstChild.insertBefore(item, sep);
}
if (len == 0) {
item = document.createElement('a');
item.href = 'javascript:void(0);'
item.appendChild(document.createTextNode('No suggestions'));
menu.firstChild.insertBefore(item, sep);
}
var n;
for (n = 0; n < webFXSpellCheckHandler.instances.length; n++) {
if (webFXSpellCheckHandler.instances[n].doc == el.ownerDocument) {
frame = webFXSpellCheckHandler.instances[n].el;
doc = webFXSpellCheckHandler.instances[n].doc;
} }
x = 0; y = 0;
for (o = frame; o; o = o.offsetParent) {
x += (o.offsetLeft - o.scrollLeft);
y += (o.offsetTop - o.scrollTop);
}
if (document.all) {
menu.style.left = x + (e.pageX || e.clientX) + 'px';
menu.style.top = y + (e.pageY || e.clientY) + (el.offsetHeight/2) + 'px';
}
else {
menu.style.left = x + ((e.pageX || e.clientX) - document.body.scrollLeft) + 'px';
menu.style.top = y + ((e.pageY || e.clientY) - document.body.scrollTop) + (el.offsetHeight/2) + 'px';
}
menu.style.display = 'block';
webFXSpellCheckHandler.activeWord = word;
};
aytv.prototype.show_menu = function(e,ak) {
var bb, bi, co = document.getElementById(ak), fv = this.ad.firstChild.childNodes.length;
while (fv > 2) { this.ad.firstChild.removeChild(this.ad.firstChild.firstChild); fv--; }
var cs = co.innerHTML;
bi = aytk.m[cs].ce.split(', ');
fv = bi.length;
if (fv > 10) { fv = 10; }
for (var i = fv - 1; i >=0; i--) {
bb = document.createElement('a');
if (aytaa) { bb.href = 'javascript:aytk.replace_word("' + ak + '", "'+bi[i]+'");' }
else { bb.href = 'javascript:oSpell.replace_word("' + ak + '", "' + bi[i] + '");' }
bb.appendChild(document.createTextNode(bi[i]));
this.ad.firstChild.insertBefore(bb,this.ad.firstChild.firstChild);
}
if (aytk.m[cs].ce == '' ) {
bb = document.createElement('a');
bb.href = 'javascript:void(0);';
bb.style.color='gray';
bb.appendChild(document.createTextNode('- No suggestions -'));
this.ad.firstChild.insertBefore(bb,this.ad.firstChild.firstChild);
}
var db = 0, da = 0;
if ((document.documentElement) && (document.documentElement.scrollTop)) {
db = e.clientX + document.documentElement.scrollLeft;
da = e.clientY + document.documentElement.scrollTop;
}
else if (document.body) {
db = e.clientX + document.body.scrollLeft;
da = e.clientY + document.body.scrollTop;
}
else {
db = e.pageX;
da = e.pageY;
}
if (document.all) {
this.ad.style.left = db + 'px';
this.ad.style.top = da + (co.offsetHeight/2) + 'px';
}
else {
this.ad.style.left = db + 'px';
this.ad.style.top = da + (co.offsetHeight/2) + 'px';
}
this.ad.style.display = 'block';
}
Code for populating the menu, the data it’s populated with is accessed i bit differently but the menu is cleared, populated and positioned in the same way. Even the ‘No suggestions’ label is the same.
Get Selection Code
WebFXLiteSpellChecker.prototype._getSelection = function() {
if (document.all) {
var sr, r, offset;
sr = document.selection.createRange();
r = sr.duplicate();
r.moveToElementText(this.elText);
r.setEndPoint('EndToEnd', sr);
this._start = r.text.length - sr.text.length;
this._end = this._start + sr.text.length;
}
else {
this._start = this.elText.selectionStart;
this._end = this.elText.selectionEnd;
}
};
aytm.prototype.ch = function(return_is_selected) {
if (document.all){
var cn, es, offset;
cn = document.selection.createRange();
es = cn.duplicate();
es.moveToElementText(this.a);
es.setEndPoint('EndToEnd',cn);
var gv = es.text.replace(/rn/g,'n'), fu = cn.text.replace(/rn/g,'n');
if (return_is_selected) { return fu.length; }
else { return gv.length - fu.length; }
}
else {
if (return_is_selected) { return (this.a.selectionEnd - this.a.selectionStart); }
else { return this.a.selectionStart; }
}
};
Code for determining selection. The original method determines the start and end position while the SpellingCow one returns either the length of the selection or the start position, depending on the parameter.
Although it’s not an exact match there’s a striking resemblance, the SpellingCow one even has the same unused offset variable that I forgot to remove while testing it.
Node Updating
i = 0;
startNode = endNode = null;
for (node = this.elCont.firstChild; node; node = node.nextSibling) {
if (node.nodeType == 1) {
str = (node.firstChild)?node.firstChild.nodeValue:'n';
}
else { str = node.nodeValue; }
n = str.length;
if ((startNode == null) && (i + n >= startPos)) {
startNode = node;
word = str.substr(0, startPos - i);
}
if (i + n >= endPos) {
endNode = node.nextSibling;
word += str.substr(endPos - i, n - (endPos - i));
break;
}
i += n;
}
var ar = this.r.firstChild, bl = 0, fi = '';
for (i = 0; ar; ar = ar.nextSibling ){
if (ar.nodeType == 1){
fi = (ar.firstChild)?ar.firstChild.nodeValue:'n';
}
else { fi = ar.nodeValue; };
bl += fi.length;
if (bl >= this.ag) {
return ((ar.className) && (ar.className == ‘red_span’))?ar.id:'’;
}
}
This one is not so obvious, it’s a part of the logic that determines where a change has been made. It loops through nodes and find the first and last word affected by counting characters. The SpellingCow one only includes the check for the last word.
This is just a few of the obvious similarities, there are plenty more.
I’m not saying that they’ve copied my LiteSpellChecker implementation, there are far too many differences in key parts of it for that.
However it seems quite likely that they’ve used my implementation as a starting point, and in some places it remains unchanged and shines through.
It’s not that I mind people using and extending my work, that is in fact exactly why I chose to talk about it and to release it under an open source licence. However removing the copyright notice is not okay and I can’t believe people still think they can get away with it simply by obfuscating the code.
And why? The MIT license clearly allows royalty free commercial usage, all that is required is that the copyright notice is left intact.
Update
Turns out they actually give me some credit:
The red squiggley was produced by Emil A Eklund who also inspired much of the mechanics of SpellingCow. This is my omage to him. Thanks!!
That’s pretty much all it takes, just wish it wasn’t tucked away in the installation instructions section…
Update 2:
See the followup.