Emil’s Chronicle - The journal of Emil A Eklund

SpellingCow

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
Screenshot of my LiteSpellChecker

SpellingCow
Screenshot of 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.

5 Responses to 'SpellingCow'

  1. Chris Says:

    Emil, is your spell checker still under development?

  2. Emil Says:

    Chris: Depends on how you look at it. I haven’t actually touched the code in about a year however I’ve been meaning to. There will be an update I’m just not sure if I’ll go with the LiteSpellChecker or RichSpellChecker approach and then there’s the question of when…

  3. Craig Nuttall Says:

    Hey All,

    I had posted directly on the thread about the lite spell checker so I had not even seen this story on Emil’s first page. I’ve already apologized to Emil via email and would like to do so again in public.

    Emil definitely did inspire the as-you-type SpellingCow version. I have a bookmark on my web browser called “HOLY SH@T!!!!” from the day I came across his site ;-) Emil is correct in that certain notices need to be present and credit given where due. I had intentionally left pieces from the menu system in there kinda to prove that Emil was my inspiration here. The menu is a simple piece that doesn’t need a lot of changing, so it’s kind of a great place to leave an “Easter Egg Ohmage” if you will. I even have an option for displaying his red squiggly too.

    But the fact is I had not yet taken the time to properly give him credit, which hadn’t seemed like a big deal since I still have 10,000 other things to do and no one knew about SpellingCow anyway. And then I’m on the front page of slashdot.org…. and it’s an obvious and glaring oversight.

    So let me say again, thank you Emil for the ground breaking work, I will be sure to get the proper credits up this week, and I’m very sorry for not having done so sooner!

    -Craig

    Craig Nuttall
    SpellingCow.com

  4. Chris Says:

    I see Emil, here’s to hoping you find the time :)

    The rich spelling approach would be brilliant if possible, something that hooks into fckeditor for example would be … well .. wow.

  5. Shachi Says:

    Emil, how do you create those strange things(like the yellow highlight and the squiggly underline) in a textarea element? I am really curious to know that.

Leave a Reply

(required)
(required, will not be published)