codemirror-typo
Version:
:snail: Spellcheck your CodeMirror editor in style
131 lines (111 loc) • 3.62 kB
JavaScript
const path = require('path');
const objectAssign = require('object-assign');
const Typo = require('typo-js');
const typoTools = require('./typo-tools');
const gutterClick = require('./gutter-menu');
function codeMirrorTypo(cm, lang, options) {
if (!cm | !lang) {
return;
}
// config defaults
let config = {
dictPath: 'dictionary/' + lang,
dictFolder: '',
filename: lang,
affFile: '',
dicFile: '',
typoClass: 'typo', // will be prefixed with 'cm-'
typoCSS: 'background: rgba(255,0,0,.25)',
excludeClasses: ['url', 'comment', 'tag', 'word'],
gutterClass: 'typoFlags',
gutterMarkClass: 'typoMark'
};
// config overwrites
if (typeof options === 'string') {
config.dictPath = options;
} else if (typeof options === 'object') {
config = objectAssign(config, options);
}
const dictPath = path.join(config.dictPath, config.dictFolder, '/');
const affFile = config.affFile ? config.affFile : config.filename;
const dicFile = config.dicFile ? config.dicFile : config.filename;
const paths = [dictPath + affFile + '.aff',
dictPath + dicFile + '.dic'];
let dict;
Promise.all(paths.map(typoTools.get)).then(data => {
dict = new Typo(lang, data[0], data[1], {platform: 'any'});
const wordSeparators = ' `~!@#$%^&*()-_=+[]{}\\|;:"<>,./?©';
try {
// major credit to @kofifus for the overlay
// https://plnkr.co/edit/x0y0RSAR9PaYXbH7PSoY?p=preview
cm.addOverlay({
token: stream => {
let ch = stream.peek();
let word = '';
if (wordSeparators.indexOf(ch) >= 0) {
stream.next();
return null;
}
while ((ch = stream.peek()) && wordSeparators.indexOf(ch) < 0) {
word += ch;
stream.next();
}
if (!dict.check(word)) {
return config.typoClass;
}
}
});
} catch (err) {
console.error(err);
}
});
typoTools.insertCSS(config);
// gutter markers for typos
const typoMark = config => {
const flag = document.createElement('i');
flag.className = config.gutterMarkClass;
flag.innerHTML = '🐌';
return flag;
};
// filter for recognizing typo highlights
const isTypoFilter = style => {
if (typeof style === 'string') {
if (style.indexOf(config.typoClass) >= 0) {
for (let i = 0; i < config.excludeClasses.length; i++) {
if (style.indexOf(config.excludeClasses[i]) >= 0) {
return false;
}
}
return true;
}
}
return false;
};
const markTypos = (editor, line) => {
if (line.styles) {
const lineTypos = line.styles.filter(isTypoFilter);
if (lineTypos.length > 0) {
editor.setGutterMarker(line, config.gutterClass, typoMark(config));
} else {
editor.setGutterMarker(line, config.gutterClass, null);
}
} else {
editor.setGutterMarker(line, config.gutterClass, null);
}
};
// iterate through lines, gutter mark ones containing typos
const markAllLines = editor => {
try {
editor.eachLine(line => markTypos(editor, line));
} catch (err) {
console.log(err);
}
};
cm.setOption('gutters', cm.getOption('gutters').concat(config.gutterClass));
cm.refresh();
// cm.on('focus', editor => markAllLines(editor));
cm.on('cursorActivity', editor => markAllLines(editor));
cm.on('gutterClick', (editor, lineN, gut, e) =>
gutterClick(editor, lineN, gut, e, config, dict));
}
module.exports = codeMirrorTypo;