sawa
sawa

Reputation: 168091

CodeMirror with spell checker

I would like to use the functionality of CodeMirror (such as linenumbering, wrapping, search, etc.) for plain text, without particular need of code highlightening but instead with Google Chrome spell checker or some other natural language (especially English) spell checking activated (I do not need to have it work on other browsers). How can I do this? Is it possible to write a plain text mode add-on that enables spell checking?

Upvotes: 19

Views: 11810

Answers (7)

Gillespie
Gillespie

Reputation: 6561

CodeMirror 6:

const state = EditorState.create({
    doc: "This is a mispellled word",
    extensions: [EditorView.contentAttributes.of({ spellcheck: 'true' })]
});

const view = new EditorView({
    state: state ,
    parent: document.getElementById('myEditor'),
});

Upvotes: 1

Flux
Flux

Reputation: 10920

In CodeMirror 5.18.0 and above, you can set inputStyle: 'contenteditable' with spellcheck: true to be able to use your web browser's spellcheck features. For example:

var myTextArea = document.getElementById('my-text-area');
var editor = CodeMirror.fromTextArea(myTextArea, {
    inputStyle: 'contenteditable',
    spellcheck: true,
});

The relevant commits that made this solution possible are:

Upvotes: 6

kofifus
kofifus

Reputation: 19285

I created a spellchecker with typos suggestions/corrections:

https://gist.github.com/kofifus/4b2f79cadc871a29439d919692099406

demo: https://plnkr.co/edit/0y1wCHXx3k3mZaHFOpHT

Below are relevant parts of the code:

First I make a promise to get load the dictionaries. I use typo.js for the dictionary, loading can take a while if they are not hosted locally, so it is better to start the loading as soon as your starts before login/CM initializing etc:

function loadTypo() {
    // hosting the dicts on your local domain will give much faster results
    const affDict='https://rawgit.com/ropensci/hunspell/master/inst/dict/en_US.aff';
    const dicDict='https://rawgit.com/ropensci/hunspell/master/inst/dict/en_US.dic';

    return new Promise(function(resolve, reject) {
        var xhr_aff = new XMLHttpRequest();
        xhr_aff.open('GET', affDict, true);
        xhr_aff.onload = function() {
            if (xhr_aff.readyState === 4 && xhr_aff.status === 200) {
                //console.log('aff loaded');
                var xhr_dic = new XMLHttpRequest();
                xhr_dic.open('GET', dicDict, true);
                xhr_dic.onload = function() {
                    if (xhr_dic.readyState === 4 && xhr_dic.status === 200) {
                        //console.log('dic loaded');
                        resolve(new Typo('en_US', xhr_aff.responseText, xhr_dic.responseText, { platform: 'any' }));
                    } else {
                        console.log('failed loading aff');
                        reject();
                    }
                };
                //console.log('loading dic');
                xhr_dic.send(null);
            } else {
                console.log('failed loading aff');
                reject();
            }
        };
        //console.log('loading aff');
        xhr_aff.send(null);
    });
}

Second I add an overlay to detect and mark typos like this:

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);

Third I use a list box to show suggestions and fix typos:

function getSuggestionBox(typo) {
    function sboxShow(cm, sbox, items, x, y) {
        let selwidget=sbox.children[0];

        let options='';
        if (items==='hourglass') {
            options='<option>&#8987;</option>'; // hourglass
        } else {
            items.forEach(s => options += '<option value="' + s + '">' + s + '</option>');
            options+='<option value="##ignoreall##">ignore&nbsp;all</option>';
        }
        selwidget.innerHTML=options;
        selwidget.disabled=(items==='hourglass');
        selwidget.size = selwidget.length;
        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';  
    }

    // 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;
            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, 'hourglass', e.pageX, e.pageY);

            // let  the ui refresh with the hourglass & show suggestions
            setTimeout(() => { 
                sboxShow(cm, sbox, typo.suggest(token), e.pageX, e.pageY); // typo.suggest takes a while
            }, 100);

            e.preventDefault();
            return false;
        });

        sbox.onmouseleave=(e => { 
            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;
}

Hope this helps !

Upvotes: 1

Doug Blank
Doug Blank

Reputation: 2279

This is a working version of hsk81's answer. It uses CodeMirror's overlay mode, and looks for any word inside quotes, html tags, etc. It has a sample typo.check that should be replaced with something like Typo.js. It underlines unknown words with a red squiggly line.

This was tested using an IPython's %%html cell.

<style>
.CodeMirror .cm-spell-error {
     background: url("https://raw.githubusercontent.com/jwulf/typojs-project/master/public/images/red-wavy-underline.gif") bottom repeat-x;
}
</style>

<h2>Overlay Parser Demo</h2>
<form><textarea id="code" name="code">
</textarea></form>

<script>
var typo = { check: function(current) {
                var dictionary = {"apple": 1, "banana":1, "can't":1, "this":1, "that":1, "the":1};
                return current.toLowerCase() in dictionary;
            }
}

CodeMirror.defineMode("spell-check", function(config, parserConfig) {
    var rx_word = new RegExp("[^\!\"\#\$\%\&\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~\ ]");
    var spellOverlay = {
        token: function (stream, state) {
          var ch;
          if (stream.match(rx_word)) { 
            while ((ch = stream.peek()) != null) {
                  if (!ch.match(rx_word)) {
                    break;
                  }
                  stream.next();
            }
            if (!typo.check(stream.current()))
                return "spell-error";
            return null;
          }
          while (stream.next() != null && !stream.match(rx_word, false)) {}
          return null;
        }
    };

  return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || "text/html"), spellOverlay);
});

var editor = CodeMirror.fromTextArea(document.getElementById("code"), {mode: "spell-check"});
</script>

Upvotes: 3

hsk81
hsk81

Reputation: 792

I actually integrated typo.js with CodeMirror while coding for NoTex.ch; you can have a look at it here CodeMirror.rest.js; I needed a way to get the reStructuredText markup spell checked, and since I use CodeMirror's excellent syntax highlighting capabilities, it was quite straight forward to do.

You can check the code at the provided link, but I'll summarize, what I've done:

  1. Initialize the typo.js library; see also the author's blog/documentation:

    var typo = new Typo ("en_US", AFF_DATA, DIC_DATA, {
        platform: 'any'
    });
    
  2. Define a regular expression for your word separators:

    var rx_word = "!\"#$%&()*+,-./:;<=>?@[\\\\\\]^_`{|}~";
    
  3. Define an overlay mode for CodeMirror:

    CodeMirror.defineMode ("myoverlay", function (config, parserConfig) {
        var overlay = {
            token: function (stream, state) {
    
                if (stream.match (rx_word) &&
                    typo && !typo.check (stream.current ()))
    
                    return "spell-error"; //CSS class: cm-spell-error
    
                while (stream.next () != null) {
                    if (stream.match (rx_word, false)) return null;
                }
    
                return null;
            }
        };
    
        var mode = CodeMirror.getMode (
            config, parserConfig.backdrop || "text/x-myoverlay"
        );
    
        return CodeMirror.overlayMode (mode, overlay);
    });
    
  4. Use the overlay with CodeMirror; see the user manual to figure out how exactly you do this. I've done it in my code so you could check it out there too, but I recommend the user manual.

  5. Define CSS class:

    .CodeMirror .cm-spell-error {
         background: url(images/red-wavy-underline.gif) bottom repeat-x;
    }
    

This approach works great for German, English and Spanish. With the French dictionary typo.js seems to have some (accent) problems, and languages like Hebrew, Hungarian, and Italian - where the number of affixes is long or the dictionary is quite extensive - it does not work really, since typo.js at its current implementation uses too much memory and is too slow.

With German (and Spanish) typo.js can block the JavaScript VM for a few hundred milliseconds (but only during initialization!), so you might want to consider background threads with HTML5 web workers (see CodeMirror.typo.worker.js for an example). Further typo.js does not seem to support Unicode (due to JavaScript restrictions): At least, I did not manage to get it to work with non-Latin languages like Russian, Greek, Hindi etc.

I've not refactored the described solution into a nice separate project apart from (now quite big) NoTex.ch, but I might do it quite soon; till then you've to patch your own solution based on the above description or hinted code. I hope this helps.

Upvotes: 29

James Westgate
James Westgate

Reputation: 11444

I wrote a squiggly underline type spell checker a while ago. It needs a rewrite to be honest I was very new to JavaScript then. But the principles are all there.

https://github.com/jameswestgate/SpellAsYouType

Upvotes: 1

Doug
Doug

Reputation: 15515

CodeMirror is not based on an HTML textarea, so you can't use the built-in spell check

You could implement your own spell check for CodeMirror with something like typo.js

I don't believe anyone has done this yet.

Upvotes: 1

Related Questions