Skip to content

Instantly share code, notes, and snippets.

@winhamwr
Created July 23, 2013 14:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save winhamwr/6062607 to your computer and use it in GitHub Desktop.
Save winhamwr/6062607 to your computer and use it in GitHub Desktop.
Semi-ghetto WYMeditor spellcheck plugin
var incorrect_spelling_style = {
'border-bottom': '1px dashed #FF0000'
};
var incorrect_spelling_class = 'incorrect';
// No constants for Node.* IE
var ELEMENT_NODE = 1;
var TEXT_NODE = 3;
WYMeditor.editor.prototype.spellcheck = function() {
if (!$.browser.msie) { return; }
var wym = this;
var button_html = '<li class="wym_tools_spellcheck"><a title="Check Spelling" name="Spellcheck" href="#" style="background-image: url(' + SITE_MEDIA_URL + 'lib/wymeditor/wymeditor/plugins/spellcheck/spellcheck.gif)">Check Spelling</a></li>';
var loading_html = '<a title="Loading" href="javascript:void(0)" style="display: none; width: 20px; height: 20px; background-image: url(' + SITE_MEDIA_URL + 'images/tiny_loading.gif); background-position: center center;">&nbsp;</a>';
// add spellcheck button
$(wym._box)
.find(wym._options.toolsSelector + wym._options.toolsListSelector)
.append(button_html);
$('.wym_tools_spellcheck').append(loading_html);
$('.wym_tools_spellcheck').click(do_spellcheck);
// add div for spellcheck corrections
$('div.wym_iframe').prepend('<div class="suggestion_container" />');
// init after wym loads
$(wym._doc).ready(function() {
remove_all_highlighting(wym);
// remove highlighting when you type over a misspelled word to correct it
$(wym._doc).bind('keyup', function() {
var span = get_cursor_container() ;
if ($(span).is('span.' + incorrect_spelling_class)) {
remove_highlight(span);
}
});
});
function get_cursor_container() {
if (document.selection) { // IE
return document.selection.createRange().parentElement();
} else { // Firefox
var sel = wym._iframe.contentWindow.getSelection();
return sel.getRangeAt(0).startContainer.parentNode;
}
}
function do_spellcheck(e) {
e.preventDefault();
remove_all_highlighting(wym);
// normalize all text nodes before submitting to spellcheck
$(wym._doc.body)
.find('*')
.andSelf()
.each( function() { normalize_shallow(this); });
// get all text nodes in the body
var text_nodes = $(wym._doc.body)
.find('*')
.andSelf()
.contents()
.filter(function() {
return this.nodeType != 1 && $.trim(this.nodeValue) !== '';
});
// submit each block of text to spellcheck server by ajax
var text_array = [];
text_nodes.each(function() {
text_array.push(this.nodeValue);
});
// show loading graphic
var $link = $(this).children('a:first');
var $loading = $(this).children('a:last');
$('.wym_tools_spellcheck').unbind('click', do_spellcheck);
$link.hide();
$loading.show();
var params = {
q: text_array
};
var traditional = true;
$.post(
'/spellcheck/',
$.param(params, traditional),
function(json) {
apply_highlighting(wym, json, text_nodes);
// hide loading graphic
$loading.hide();
$link.show();
$('.wym_tools_spellcheck').click(do_spellcheck);
},
'json'
);
}
};
function wrap_node(node, wrap) {
// use raw DOM instead of jQuery wrap(), to work around an IE bug
node.parentNode.insertBefore(wrap, node);
wrap.appendChild(node);
}
function normalize_shallow(node) {
for (i=0; i<node.childNodes.length; i++) {
var child = node.childNodes[i];
if (child.nodeType != TEXT_NODE) { continue; }
if (child.nodeValue === '') {
node.removeChild(child);
i -= 1;
continue;
}
var next = child.nextSibling;
if (next === null || next.nodeType != TEXT_NODE) { continue; }
var combined_text = child.nodeValue + next.nodeValue;
new_node = node.ownerDocument.createTextNode(combined_text);
node.insertBefore(new_node, child);
node.removeChild(child);
node.removeChild(next);
i -= 1;
}
}
function pluralize(word, num, singular, plural) {
if (typeof(singular) == 'undefined') { singular = ''; }
if (typeof(plural) == 'undefined') { plural = 's'; }
if (num == 1) {
return word + singular;
} else {
return word + plural;
}
}
function apply_highlighting(wym, corrections, text_nodes) {
var num_corrections = 0;
var text_nodes_list = text_nodes.get();
var corrections_by_node_index = {};
$.each(corrections, function(index, correction) {
if (!(correction.node_index in corrections_by_node_index)) {
corrections_by_node_index[correction.node_index] = [];
}
corrections_by_node_index[correction.node_index].push(correction);
});
$.each(corrections_by_node_index, function(index, corrections) {
// Sort the corrections in descending order. We do this to process the
// text in reverse order. If we didn't, then all of the offsets would
// be incorrect after the first iteration since we modify the DOM
corrections.sort(function(a, b) { return b.offset - a.offset; });
$.each(corrections, function(index, correction) {
var current_node = text_nodes_list[correction.node_index];
// highlight the correction, by splitting the text node in three
// and wrapping the middle portion
var left = current_node;
var middle = left.splitText(correction.offset);
var right = middle.splitText(correction.length);
var highlight_span = wym._doc.createElement('span');
$(highlight_span).addClass(incorrect_spelling_class).css(incorrect_spelling_style);
wrap_node(middle, highlight_span);
add_suggestions_qtip(wym, highlight_span, correction.suggestions);
num_corrections += 1;
});
});
alert(num_corrections + pluralize(' word', num_corrections) + ' spelled incorrectly');
}
function remove_all_highlighting(wym) {
var highlights = $(wym._doc.body).find('.' + incorrect_spelling_class);
highlights.each(function() {
remove_highlight(this);
});
wym.update();
}
function remove_highlight(span) {
var span_parent = span.parentNode;
span = $(span);
span.contents().insertBefore(span); // move the contents out of the span
// delete the empty span and merge adjacent text nodes
span.remove();
normalize_shallow(span_parent);
}
function strip_spellcheck_highlight(wym) {
// remove <span> highlighting from text area
var html = wym._element.val();
var re_span = RegExp('<span class="'+incorrect_spelling_class+'.+?">(.*?)</span>', 'g');
html = html.replace(re_span, '$1');
$(wym._element).val(html);
}
function add_suggestions_qtip(wym, span, suggestions) {
// pop up a div showing suggestions
var suggestion_links = [];
$.each(suggestions, function(i, val) {
suggestion_links.push('<li>'+val+'</li>');
});
var content = '<ul class="suggestions">'+suggestion_links.join('\n')+'<ul>';
$(span).qtip({
content: { text: content },
show: { delay: 0, solo: true, when: { event: 'click' }, effect: { length: 0 } },
hide: { when: { event: 'unfocus' } },
position: {
type: 'static',
container: $('div.suggestion_container')
},
api: {
onRender: function() {
var qtip = this;
// Apply the suggestion and hide the tooltip when we click.
$(qtip.elements.content).find('ul.suggestions li').click(function() {
$(span).text($(this).text());
remove_highlight(span);
qtip.hide();
});
}
}
});
}
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('spellcheck.views',
url(r'^$', 'spellcheck', name='spellcheck'),
)
import enchant
from enchant.tokenize import get_tokenizer
from django.views.decorators.csrf import csrf_exempt
from pstat.core.utils import JSONResponseView
class SpellCheckViewJSON(JSONResponseView):
def get_context_data(self, *args, **kwargs):
text_list = self.request.POST.getlist('q')
# Create a dictionary that uses an in-memory PWL (personal word list)
# It's important to use `DictWithPWL` instead of `Dict` because if we
# use `Dict`, then a global file is created at
# $HOME/.config/enchant/en_US.dic that contains all of the words that
# we add here
d = enchant.DictWithPWL('en-US')
for word in self.request.tenant.settings.get_spellcheck_additions():
d.add(word)
# if the word is lowercase, also accept the first-letter
# capitalized version as a correct spelling.
if word == word.lower():
d.add(word.capitalize())
tokenizer = get_tokenizer('en_US')
return [
{
'length': len(word),
'suggestions': d.suggest(word),
'offset': offset,
'node_index': index,
}
for index, block in enumerate(text_list)
for word, offset in tokenizer(block)
if not d.check(word)
]
spellcheck = csrf_exempt(SpellCheckViewJSON.as_view())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment