mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-11 00:36:26 +00:00
256 lines
7.2 KiB
JavaScript
256 lines
7.2 KiB
JavaScript
import styles from '../components/CodeEditor.styl'
|
|
import i18n from 'browser/lib/i18n'
|
|
|
|
const Typo = require('typo-js')
|
|
const _ = require('lodash')
|
|
|
|
const CSS_ERROR_CLASS = 'codeEditor-typo'
|
|
const SPELLCHECK_DISABLED = 'NONE'
|
|
const DICTIONARY_PATH = '../dictionaries'
|
|
const MILLISECONDS_TILL_LIVECHECK = 500
|
|
|
|
let dictionary = null
|
|
let self
|
|
|
|
function getAvailableDictionaries() {
|
|
return [
|
|
{ label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED },
|
|
{ label: i18n.__('English'), value: 'en_GB' },
|
|
{ label: i18n.__('German'), value: 'de_DE' },
|
|
{ label: i18n.__('French'), value: 'fr_FR' }
|
|
]
|
|
}
|
|
|
|
/**
|
|
* Only to be used in the tests :)
|
|
*/
|
|
function setDictionaryForTestsOnly(newDictionary) {
|
|
dictionary = newDictionary
|
|
}
|
|
|
|
/**
|
|
* @description Initializes the spellcheck. It removes all existing marks of the current editor.
|
|
* If a language was given (i.e. lang !== this.SPELLCHECK_DISABLED) it will load the stated dictionary and use it to check the whole document.
|
|
* @param {Codemirror} editor CodeMirror-Editor
|
|
* @param {String} lang on of the values from getAvailableDictionaries()-Method
|
|
*/
|
|
function setLanguage(editor, lang) {
|
|
self = this
|
|
dictionary = null
|
|
|
|
if (editor == null) {
|
|
return
|
|
}
|
|
|
|
const existingMarks = editor.getAllMarks() || []
|
|
for (const mark of existingMarks) {
|
|
mark.clear()
|
|
}
|
|
if (lang !== SPELLCHECK_DISABLED) {
|
|
dictionary = new Typo(lang, false, false, {
|
|
dictionaryPath: DICTIONARY_PATH,
|
|
asyncLoad: true,
|
|
loadedCallback: () => checkWholeDocument(editor)
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks the whole content of the editor for typos
|
|
* @param {Codemirror} editor CodeMirror-Editor
|
|
*/
|
|
function checkWholeDocument(editor) {
|
|
const lastLine = editor.lineCount() - 1
|
|
const textOfLastLine = editor.getLine(lastLine) || ''
|
|
const lastChar = textOfLastLine.length
|
|
const from = { line: 0, ch: 0 }
|
|
const to = { line: lastLine, ch: lastChar }
|
|
checkMultiLineRange(editor, from, to)
|
|
}
|
|
|
|
/**
|
|
* Checks the given range for typos
|
|
* @param {Codemirror} editor CodeMirror-Editor
|
|
* @param {line, ch} from starting position of the spellcheck
|
|
* @param {line, ch} to end position of the spellcheck
|
|
*/
|
|
function checkMultiLineRange(editor, from, to) {
|
|
function sortRange(pos1, pos2) {
|
|
if (
|
|
pos1.line > pos2.line ||
|
|
(pos1.line === pos2.line && pos1.ch > pos2.ch)
|
|
) {
|
|
return { from: pos2, to: pos1 }
|
|
}
|
|
return { from: pos1, to: pos2 }
|
|
}
|
|
|
|
const { from: smallerPos, to: higherPos } = sortRange(from, to)
|
|
for (let l = smallerPos.line; l <= higherPos.line; l++) {
|
|
const line = editor.getLine(l) || ''
|
|
let w = 0
|
|
if (l === smallerPos.line) {
|
|
w = smallerPos.ch
|
|
}
|
|
let wEnd = line.length
|
|
if (l === higherPos.line) {
|
|
wEnd = higherPos.ch
|
|
}
|
|
while (w <= wEnd) {
|
|
const wordRange = editor.findWordAt({ line: l, ch: w })
|
|
self.checkWord(editor, wordRange)
|
|
w += wordRange.head.ch - wordRange.anchor.ch + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description Checks whether a certain range of characters in the editor (i.e. a word) contains a typo.
|
|
* If so the ranged will be marked with the class CSS_ERROR_CLASS.
|
|
* Note: Due to performance considerations, only words with more then 3 signs are checked.
|
|
* @param {Codemirror} editor CodeMirror-Editor
|
|
* @param wordRange Object specifying the range that should be checked.
|
|
* Having the following structure: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
|
|
*/
|
|
function checkWord(editor, wordRange) {
|
|
const word = editor.getRange(wordRange.anchor, wordRange.head)
|
|
if (word == null || word.length <= 3) {
|
|
return
|
|
}
|
|
if (!dictionary.check(word)) {
|
|
editor.markText(wordRange.anchor, wordRange.head, {
|
|
className: styles[CSS_ERROR_CLASS]
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks the changes recently made (aka live check)
|
|
* @param {Codemirror} editor CodeMirror-Editor
|
|
* @param fromChangeObject codeMirror changeObject describing the start of the editing
|
|
* @param toChangeObject codeMirror changeObject describing the end of the editing
|
|
*/
|
|
function checkChangeRange(editor, fromChangeObject, toChangeObject) {
|
|
/**
|
|
* Calculate the smallest respectively largest position as a start, resp. end, position and return it
|
|
* @param start CodeMirror change object
|
|
* @param end CodeMirror change object
|
|
* @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
|
|
*/
|
|
function getStartAndEnd(start, end) {
|
|
const possiblePositions = [start.from, start.to, end.from, end.to]
|
|
let smallest = start.from
|
|
let biggest = end.to
|
|
for (const currentPos of possiblePositions) {
|
|
if (
|
|
currentPos.line < smallest.line ||
|
|
(currentPos.line === smallest.line && currentPos.ch < smallest.ch)
|
|
) {
|
|
smallest = currentPos
|
|
}
|
|
if (
|
|
currentPos.line > biggest.line ||
|
|
(currentPos.line === biggest.line && currentPos.ch > biggest.ch)
|
|
) {
|
|
biggest = currentPos
|
|
}
|
|
}
|
|
return { start: smallest, end: biggest }
|
|
}
|
|
|
|
if (dictionary === null || editor == null) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const { start, end } = getStartAndEnd(fromChangeObject, toChangeObject)
|
|
|
|
// Expand the range to include words after/before whitespaces
|
|
start.ch = Math.max(start.ch - 1, 0)
|
|
end.ch = end.ch + 1
|
|
|
|
// clean existing marks
|
|
const existingMarks = editor.findMarks(start, end) || []
|
|
for (const mark of existingMarks) {
|
|
mark.clear()
|
|
}
|
|
|
|
self.checkMultiLineRange(editor, start, end)
|
|
} catch (e) {
|
|
console.info(
|
|
'Error during the spell check. It might be due to problems figuring out the range of the new text..',
|
|
e
|
|
)
|
|
}
|
|
}
|
|
|
|
function saveLiveSpellCheckFrom(changeObject) {
|
|
liveSpellCheckFrom = changeObject
|
|
}
|
|
let liveSpellCheckFrom
|
|
const debouncedSpellCheckLeading = _.debounce(
|
|
saveLiveSpellCheckFrom,
|
|
MILLISECONDS_TILL_LIVECHECK,
|
|
{
|
|
leading: true,
|
|
trailing: false
|
|
}
|
|
)
|
|
const debouncedSpellCheck = _.debounce(
|
|
checkChangeRange,
|
|
MILLISECONDS_TILL_LIVECHECK,
|
|
{
|
|
leading: false,
|
|
trailing: true
|
|
}
|
|
)
|
|
|
|
/**
|
|
* Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
|
|
* @param {Codemirror} editor CodeMirror-Editor
|
|
* @param changeObject codeMirror changeObject
|
|
*/
|
|
function handleChange(editor, changeObject) {
|
|
if (dictionary === null) {
|
|
return
|
|
}
|
|
debouncedSpellCheckLeading(changeObject)
|
|
debouncedSpellCheck(editor, liveSpellCheckFrom, changeObject)
|
|
}
|
|
|
|
/**
|
|
* Returns an array of spelling suggestions for the given (wrong written) word.
|
|
* Returns an empty array if the dictionary is null (=> spellcheck is disabled) or the given word was null
|
|
* @param word word to be checked
|
|
* @returns {String[]} Array of suggestions
|
|
*/
|
|
function getSpellingSuggestion(word) {
|
|
if (dictionary == null || word == null) {
|
|
return []
|
|
}
|
|
return dictionary.suggest(word)
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the CSS class used for errors
|
|
*/
|
|
function getCSSClassName() {
|
|
return styles[CSS_ERROR_CLASS]
|
|
}
|
|
|
|
module.exports = {
|
|
DICTIONARY_PATH,
|
|
CSS_ERROR_CLASS,
|
|
SPELLCHECK_DISABLED,
|
|
getAvailableDictionaries,
|
|
setLanguage,
|
|
checkChangeRange,
|
|
handleChange,
|
|
getSpellingSuggestion,
|
|
checkWord,
|
|
checkMultiLineRange,
|
|
checkWholeDocument,
|
|
setDictionaryForTestsOnly,
|
|
getCSSClassName
|
|
}
|