Last active
May 16, 2024 13:05
-
-
Save kofifus/4b2f79cadc871a29439d919692099406 to your computer and use it in GitHub Desktop.
CodeMirror spell checker with typo correction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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"> ⌛</option>'; | |
if (separator) options += '<option style="min-height:1px; max-height:1px; padding:0; background-color: #000000;" disabled> </option>'; | |
options += '<option value="##ignoreall##">Ignore 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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sorry can't help .. good luck :)