【问题标题】:CodeMirror with spell checker带有拼写检查器的 CodeMirror
【发布时间】:2012-09-02 20:40:10
【问题描述】:

我想将CodeMirror 的功能(例如行号、换行、搜索等)用于纯文本,而不需要特别强调代码,而是使用 Google Chrome 拼写检查器或其他一些自然语言(尤其是英文)拼写检查已激活(我不需要让它在其他浏览器上工作)。我怎样才能做到这一点?是否可以编写一个启用拼写检查的纯文本模式插件?

【问题讨论】:

  • 这里的大多数答案都严重过时了。现在 CodeMirror 中的拼写检查很容易。参考我的回答:stackoverflow.com/a/66641365

标签: javascript textarea codemirror


【解决方案1】:

我实际上在为NoTex.ch 编码时将typo.jsCodeMirror 集成在一起;你可以在这里查看CodeMirror.rest.js;我需要一种方法来检查 reStructuredText 标记的拼写,而且由于我使用 CodeMirror 出色的语法高亮功能,所以这很简单。

您可以在提供的链接中查看代码,但我会总结一下我所做的:

  1. 初始化typo.js 库;另请参阅作者的博客/文档:

    var typo = new Typo ("en_US", AFF_DATA, DIC_DATA, {
        platform: 'any'
    });
    
  2. 为您的单词分隔符定义一个正则表达式:

    var rx_word = "!\"#$%&()*+,-./:;<=>?@[\\\\\\]^_`{|}~";
    
  3. 为 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. 将覆盖与 CodeMirror 一起使用;请参阅用户手册以了解您是如何做到这一点的。我已经在我的代码中完成了,所以你也可以在那里查看,但我推荐使用用户手册。

  5. 定义 CSS 类:

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

这种方法适用于德语、英语和西班牙语。法语词典 typo.js 似乎有一些(口音)问题,而像希伯来语、匈牙利语和意大利语这样的语言 - 词缀的数量很长或字典非常广泛 - 它没有确实有效,因为 typo.js 在其当前实现中使用了太多内存并且太慢了。

使用德语(和西班牙语)typo.js 可以阻止 JavaScript VM 几百毫秒(但仅限于初始化期间!),因此您可能需要考虑使用 HTML5 Web Worker 的后台线程(参见 CodeMirror.typo.worker.js 示例)。此外,typo.js 似乎不支持 Unicode(由于 JavaScript 限制):至少,我没有设法让它与俄语、希腊语、印地语等非拉丁语言一起使用。

除了(现在相当大)NoTex.ch 之外,我还没有将所描述的解决方案重构为一个很好的独立项目,但我可能很快就会这样做;在此之前,您必须根据上述描述或提示代码修补您自己的解决方案。我希望这会有所帮助。

【讨论】:

  • 这太棒了!您可能还想使用新的 addOverlay 功能 (codemirror.net/doc/manual.html#addOverlay),它提供了一种更有效且侵入性更小的方式来添加实用程序覆盖。
  • 这个答案中的链接不再解析,文件被移动了,它被杀死了吗?
  • 很高兴能帮上忙;如果您对该方法有任何疑问,请随时直接与我联系;此外,NoTex.ch 上提供了一个实时示例。(此链接现在应该可以使用;有人替换了正确的链接)。
  • 该正则表达式中不应该有空格吗?我做了这样的事情:吃空格:if (stream.match(/^\W+/)) return;拼写检查一个字:if (stream.match(/^\w+/) &amp;&amp; !typo.check(stream.current())) return 'spell-error';
  • 对于那些将来遇到这个答案的人,我已经根据这个答案创建了一个工作解决方案,并将它捆绑到一个不错的 CodeMirror 插件中。查看github.com/NextStepWebs/codemirror-spell-checker
【解决方案2】:

这是 hsk81 答案的工作版本。它使用 CodeMirror 的覆盖模式,并在引号、html 标记等中查找任何单词。它有一个示例 Typo.check,应该用 Typo.js 之类的东西替换。它用红色波浪线在未知单词下划线。

这是使用 IPython 的 %%html 单元格测试的。

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

【讨论】:

    【解决方案3】:

    在 CodeMirror 5.18.0 及更高版本中,您可以将inputStyle: 'contenteditable' 设置为spellcheck: true,以便能够使用您的网络浏览器的拼写检查功能。例如:

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

    使该解决方案成为可能的相关提交是:

    【讨论】:

      【解决方案4】:

      CodeMirror 不是基于 HTML 文本区域,所以你 can't use the built-in spell check

      您可以使用 typo.js 之类的方式对 CodeMirror 实施自己的拼写检查

      我相信还没有人做过。

      【讨论】:

      • 好吧,忘记我之前的评论。您建议的线条似乎更笼统,我会研究一下。
      • CodeMirror 面向代码,而不是散文。因此,如果您不需要语法高亮,它可能不是正确的工具。你可能想看看MarkItUp,它有一个proof of concept spell checker
      • 您的答案有助于寻找正确的方向,但 hsk81 给出了更详细的答案,因此我将复选标记移至该答案。谢谢。
      【解决方案5】:

      我不久前写了一个波浪形下划线类型的拼写检查器。老实说,它需要重写,那时我对 JavaScript 还很陌生。但原则都在那里。

      https://github.com/jameswestgate/SpellAsYouType

      【讨论】:

        【解决方案6】:

        我创建了一个带有拼写错误建议/更正的拼写检查器:

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

        演示:https://plnkr.co/edit/0y1wCHXx3k3mZaHFOpHT

        以下是代码的相关部分:

        首先,我承诺加载词典。我使用typo.js作为字典,如果它们不在本地托管,加载可能需要一段时间,所以最好在登录/CM初始化等之前开始加载:

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

        其次,我添加一个叠加层来检测和标记这样的错别字:

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

        第三,我使用列表框来显示建议并修正拼写错误:

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

        希望这会有所帮助!

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-04-26
          • 1970-01-01
          • 2016-11-09
          • 1970-01-01
          • 1970-01-01
          • 2013-04-15
          • 1970-01-01
          • 2011-07-22
          相关资源
          最近更新 更多