CodeMirror with spell checker
Asked Answered
A

7

19

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?

Austine answered 9/9, 2012 at 23:47 Comment(1)
Most of the answers here are severely outdated. Spellcheck is now easy in CodeMirror. Refer to my answer: stackoverflow.com/a/66641365Gastrology
W
29

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.

Worked answered 17/9, 2012 at 15:43 Comment(6)
This is awesome! You might also want to use the new addOverlay feature (codemirror.net/doc/manual.html#addOverlay), which provides a more efficient and less invasive way of adding utility overlays.Leanaleanard
the link in this answer no longer resolves, was the file moved, it has it been killed off?Prejudicial
I'm glad I could help; if you've a problem with the approach just feel free to contact me directly; further, a live example is available at NoTex.ch .. (this links should now work; somebody had replaced the correct link).Worked
Shouldn't there be a space in that regex? I did something like this: Eat whitespace: if (stream.match(/^\W+/)) return; Spellcheck one word: if (stream.match(/^\w+/) && !typo.check(stream.current())) return 'spell-error';Lepsy
Mmh, this looks very interesting: my guess is that the lack of the space character is not relevant due to CM's tokenization of the text input (not sure though since I've written the regex back in 2013-02-28). But I'd say that if /^\W+/ is a super set of "!\"#$%&()*+,-./:;<=>?@[\\\\\\]^_{|}~"` (plus white space) then you're good. Just test accordingly and you'll find the definitive answer.Worked
For those coming across this answer in the future, I've created a working solution based on this answer and bundled it into a nice plugin for CodeMirror. Check it out at github.com/NextStepWebs/codemirror-spell-checkerKevakevan
G
6

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:

Gastrology answered 15/3, 2021 at 15:54 Comment(2)
doesn't seem to work in codemirror 6 :(Mandolin
@Mandolin See my answer for codemirror 6 -- https://mcmap.net/q/633262/-codemirror-with-spell-checkerAffirmatory
C
3

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>
Cassondracassoulet answered 16/7, 2014 at 12:16 Comment(0)
F
1

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.

Fibre answered 14/9, 2012 at 3:6 Comment(3)
Okay, forget about my previous comment. The lines you suggested seems more general, and I will look into it.Austine
CodeMirror is oriented towards code, not prose. So if you don't need syntax highlighting, it may not be the right tool. You may want to look at MarkItUp which has a proof of concept spell checkerFibre
Your answer helped in looking into the right direction, but hsk81 gave an even more detailed answer, so I move the checkmark to that answer. Thanks.Austine
M
1

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

Mixtec answered 17/9, 2012 at 10:46 Comment(0)
A
1

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 !

Appointee answered 29/7, 2016 at 1:14 Comment(0)
A
1

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'),
});
Affirmatory answered 15/2 at 0:50 Comment(2)
oooo I don't have time to check if this works now but I've added a TODO in my project to give it a whirl - excited if it does work - thx so much :)Mandolin
Working for me in Chrome and Firefox. I noticed sometimes I have to focus on the codemirror editor (i.e. click inside it) before the red squiggles appear under misspelled words.Affirmatory

© 2022 - 2024 — McMap. All rights reserved.