Skip to content

Instantly share code, notes, and snippets.

@kofifus
Last active September 23, 2022 17:12
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kofifus/4b2f79cadc871a29439d919692099406 to your computer and use it in GitHub Desktop.
Save kofifus/4b2f79cadc871a29439d919692099406 to your computer and use it in GitHub Desktop.
CodeMirror spell checker with typo correction
usage:
------
// include codemirror.js, addon/mode/overlay.js
// include async typo.js from https://github.com/cfinke/Typo.js/pull/45
// include loadTypo.js from: https://github.com/cfinke/Typo.js/pull/50
// loading typo + dicts takes a while so we start it first
// hosting the dicts on your local domain will give much faster loading time
// english dictionaries taken from https://github.com/cfinke/Typo.js/pull/47
// get other dictionaries with git clone https://chromium.googlesource.com/chromium/deps/hunspell_dictionaries
const aff = 'https://cdn.rawgit.com/kofifus/Typo.js/312bf158a814dda6eac3bd991e3a133c84472fc8/typo/dictionaries/en_US/en_US.aff';
const dic = 'https://cdn.rawgit.com/kofifus/Typo.js/312bf158a814dda6eac3bd991e3a133c84472fc8/typo/dictionaries/en_US/en_US.dic';
let typoLoaded=loadTypo(aff, dic);
// initialize codemirror instances etc
...
// start spellchecking
typoLoaded.then(typo => startSpellCheck(cm, typo));
demo:
-----
https://plnkr.co/edit/0y1wCHXx3k3mZaHFOpHT
.CodeMirror .cm-spell-error {
background-image: url("https://raw.githubusercontent.com/jwulf/typojs-project/master/public/images/red-wavy-underline.gif");
background-position: bottom;
background-repeat: repeat-x;
}
#suggestBox {
display:inline-block; overflow:hidden; border:solid black 1px;
}
#suggestBox > select {
padding:10px; margin:-5px -20px -5px -5px;
}
#suggestBox > select > option:hover {
box-shadow: 0 0 10px 100px #4A8CF7 inset; color: white;
}
"use strict";
function startSpellCheck(cm, typo) {
if (!cm || !typo) return; // sanity
startSpellCheck.ignoreDict = {}; // dictionary of ignored words
// Define what separates a word
var rx_word = '!\'\"#$%&()*+,-./:;<=>?@[\\]^_`{|}~ ';
cm.spellcheckOverlay = {
token: function(stream) {
var ch = stream.peek();
var word = "";
if (rx_word.includes(ch) || ch === '\uE000' || ch === '\uE001') {
stream.next();
return null;
}
while ((ch = stream.peek()) && !rx_word.includes(ch)) {
word += ch;
stream.next();
}
if (!/[a-z]/i.test(word)) return null; // no letters
if (startSpellCheck.ignoreDict[word]) return null;
if (!typo.check(word)) return "spell-error"; // CSS class: cm-spell-error
}
}
cm.addOverlay(cm.spellcheckOverlay);
// initialize the suggestion box
let sbox = getSuggestionBox(typo);
cm.getWrapperElement().oncontextmenu = (e => {
e.preventDefault();
e.stopPropagation();
sbox.suggest(cm, e);
return false;
});
}
function getSuggestionBox(typo) {
function sboxShow(cm, sbox, items, x, y, hourglass) {
let selwidget = sbox.children[0];
var isSafari = navigator.vendor && navigator.vendor.indexOf('Apple') > -1 && navigator.userAgent && !navigator.userAgent.match('CriOS');
let separator=(!isSafari && (hourglass || items.length>0)); // separator line does not work well on safari
let options = '';
items.forEach(s => options += '<option value="' + s + '">' + s + '</option>');
if (hourglass) options += '<option disabled="disabled">&nbsp;&nbsp;&nbsp;&#8987;</option>';
if (separator) options += '<option style="min-height:1px; max-height:1px; padding:0; background-color: #000000;" disabled>&nbsp;</option>';
options += '<option value="##ignoreall##">Ignore&nbsp;All</option>';
let indexInParent=[].slice.call(selwidget.parentElement.children).indexOf(selwidget);
selwidget.innerHTML=options;
selwidget=selwidget.parentElement.children[indexInParent];
let fontSize=window.getComputedStyle(cm.getWrapperElement(), null).getPropertyValue('font-size');
selwidget.style.fontSize=fontSize;
selwidget.size = selwidget.length;
if (separator) selwidget.size--;
selwidget.value = -1;
// position widget inside cm
let cmrect = cm.getWrapperElement().getBoundingClientRect();
sbox.style.left = x + 'px';
sbox.style.top = (y - sbox.offsetHeight / 2) + 'px';
let widgetRect = sbox.getBoundingClientRect();
if (widgetRect.top < cmrect.top) sbox.style.top = (cmrect.top + 2) + 'px';
if (widgetRect.right > cmrect.right) sbox.style.left = (cmrect.right - widgetRect.width - 2) + 'px';
if (widgetRect.bottom > cmrect.bottom) sbox.style.top = (cmrect.bottom - widgetRect.height - 2) + 'px';
}
function sboxHide(sbox) {
sbox.style.top = sbox.style.left = '-1000px';
typo.suggest(); // disable any running suggeations search
}
// create suggestions widget
let sbox = document.getElementById('suggestBox');
if (!sbox) {
sbox = document.createElement('div');
sbox.style.zIndex = 100000;
sbox.id = 'suggestBox';
sbox.style.position = 'fixed';
sboxHide(sbox);
let selwidget = document.createElement('select');
selwidget.multiple = 'yes';
sbox.appendChild(selwidget);
sbox.suggest = ((cm, e) => { // e is the event from cm contextmenu event
if (!e.target.classList.contains('cm-spell-error')) return false; // not on typo
let token = e.target.innerText;
if (!token) return false; // sanity
// save cm instance, token, token coordinates in sbox
sbox.codeMirror = cm;
sbox.token = token;
sbox.screenPos={ x: e.pageX, y: e.pageY }
let tokenRect = e.target.getBoundingClientRect();
let start=cm.coordsChar({left: tokenRect.left+1, top: tokenRect.top+1});
let end=cm.coordsChar({left: tokenRect.right-1, top: tokenRect.top+1});
sbox.cmpos={ line: start.line, start: start.ch, end: end.ch};
// show hourglass
sboxShow(cm, sbox, [], e.pageX, e.pageY, true);
var results = [];
// async
typo.suggest(token, null, all => {
//console.log('done');
sboxShow(cm, sbox, results, e.pageX, e.pageY);
}, next => {
//console.log('found '+next);
results.push(next);
sboxShow(cm, sbox, results, e.pageX, e.pageY, true);
});
// non async
//sboxShow(cm, sbox, typo.suggest(token), e.pageX, e.pageY);
e.preventDefault();
return false;
});
sbox.onmouseout = (e => {
let related=(e.relatedTarget ? e.relatedTarget.tagName : null);
if (related!=='SELECT' && related!=='OPTION') sboxHide(sbox)
});
selwidget.onchange = (e => {
sboxHide(sbox)
let cm = sbox.codeMirror, correction = e.target.value;
if (correction == '##ignoreall##') {
startSpellCheck.ignoreDict[sbox.token] = true;
cm.setOption('maxHighlightLength', (--cm.options.maxHighlightLength) + 1); // ugly hack to rerun overlays
} else {
cm.replaceRange(correction, { line: sbox.cmpos.line, ch: sbox.cmpos.start}, { line: sbox.cmpos.line, ch: sbox.cmpos.end});
cm.focus();
cm.setCursor({line: sbox.cmpos.line, ch: sbox.cmpos.start+correction.length});
}
});
document.body.appendChild(sbox);
}
return sbox;
}
@kofifus
Copy link
Author

kofifus commented Aug 12, 2020

Sorry can't help .. good luck :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment