mirror of
https://github.com/BoostIo/Boostnote
synced 2026-01-06 13:39:19 +00:00
Spellcheck - initialisation and first draft onChange
This commit is contained in:
@@ -10,7 +10,8 @@ import iconv from 'iconv-lite'
|
||||
import crypto from 'crypto'
|
||||
import consts from 'browser/lib/consts'
|
||||
import fs from 'fs'
|
||||
const { ipcRenderer } = require('electron')
|
||||
const {ipcRenderer} = require('electron')
|
||||
const spellcheck = require('browser/lib/spellcheck')
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
@@ -23,7 +24,7 @@ export default class CodeEditor extends React.Component {
|
||||
super(props)
|
||||
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.changeHandler = (e) => this.handleChange(e)
|
||||
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
|
||||
this.focusHandler = () => {
|
||||
ipcRenderer.send('editor:focused', true)
|
||||
}
|
||||
@@ -82,7 +83,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { rulers, enableRulers } = this.props
|
||||
const {rulers, enableRulers} = this.props
|
||||
const expandSnippet = this.expandSnippet.bind(this)
|
||||
|
||||
const defaultSnippet = [
|
||||
@@ -162,7 +163,8 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//TODO: Nur bei MarkdownNotes
|
||||
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
|
||||
this.setMode(this.props.mode)
|
||||
|
||||
this.editor.on('focus', this.focusHandler)
|
||||
@@ -203,7 +205,7 @@ export default class CodeEditor extends React.Component {
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition })
|
||||
cm.setCursor({line: cursor.line + cursorLineNumber, ch: cursorLinePosition})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -319,10 +321,11 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.autoLoadMode(this.editor, syntax.mode)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.value = this.editor.getValue()
|
||||
handleChange (editor, changeObject) {
|
||||
spellcheck.handleChange(editor, changeObject)
|
||||
this.value = editor.getValue()
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(e)
|
||||
this.props.onChange(editor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,7 +424,7 @@ export default class CodeEditor extends React.Component {
|
||||
const value = editor.getValue()
|
||||
const cursor = editor.getCursor()
|
||||
const newValue = value.replace(taggedUrl, replacement)
|
||||
const newCursor = Object.assign({}, cursor, { ch: cursor.ch + newValue.length - value.length })
|
||||
const newCursor = Object.assign({}, cursor, {ch: cursor.ch + newValue.length - value.length})
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(newCursor)
|
||||
}
|
||||
@@ -517,6 +520,26 @@ export default class CodeEditor extends React.Component {
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
createSpellCheckPanel () {
|
||||
//TODO: von spellcheck abfragen
|
||||
//TODO: l18n
|
||||
//Todo: styling
|
||||
const panel = document.createElement('div')
|
||||
panel.className = 'panel bottom'
|
||||
const dropdown = document.createElement('select')
|
||||
dropdown.title = 'spellcheck'
|
||||
dropdown.addEventListener('change', (e) => spellcheck.initialize(this.editor, dropdown.value))
|
||||
const options = [{label: 'Disabeld', value: 'NONE'}, {label: 'Deutsch', value: 'de_DE'}]
|
||||
for (const op of options) {
|
||||
const option = document.createElement('option')
|
||||
option.value = op.value
|
||||
option.innerHTML = op.label
|
||||
dropdown.appendChild(option)
|
||||
}
|
||||
panel.appendChild(dropdown)
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
|
||||
2
browser/components/CodeEditor.styl
Normal file
2
browser/components/CodeEditor.styl
Normal file
@@ -0,0 +1,2 @@
|
||||
.codeEditor-typo
|
||||
text-decoration underline wavy red
|
||||
162
browser/lib/spellcheck.js
Normal file
162
browser/lib/spellcheck.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import styles from '../components/CodeEditor.styl'
|
||||
|
||||
const Typo = require('typo-js')
|
||||
|
||||
const CSS_ERROR_CLASS = 'codeEditor-typo'
|
||||
const SPELLCHECK_DISABLED = 'NONE'
|
||||
const DICTIONARY_PATH = '../dictionaries'
|
||||
let dictionary = null
|
||||
|
||||
function getAvailableDictionaries () {
|
||||
// TODO: l18n
|
||||
return [{label: 'Disabeld', value: SPELLCHECK_DISABLED}, {label: 'Deutsch', value: 'de_DE'}]
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 initialize (editor, lang) {
|
||||
dictionary = null
|
||||
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(this, editor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the whole content of the editor for typos
|
||||
* @param thisReference a reference to <code>this</code>. Needed because this is null if called from parent method
|
||||
* @param {Codemirror} editor CodeMirror-Editor
|
||||
*/
|
||||
function checkWholeDocument (thisReference, 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(thisReference, editor, from, to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given range for typos
|
||||
* @param thisReference a reference to <code>this</code>. Needed because this is null if called from parent method
|
||||
* @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 (thisReference, editor, from, to) {
|
||||
const currentText = editor.getRange(from, to) || ''
|
||||
const lines = currentText.split('\n')
|
||||
for (let l = from.line; l <= to.line; l++) {
|
||||
const line = lines[l] || ''
|
||||
let w = 0
|
||||
if (l === from.line) {
|
||||
w = from.ch
|
||||
}
|
||||
let wEnd = line.length
|
||||
if (l === to.line) {
|
||||
wEnd = to.ch
|
||||
}
|
||||
while (w < wEnd) {
|
||||
const wordRange = editor.findWordAt({line: l, ch: w})
|
||||
thisReference.checkRange(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 checkRange (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]})
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange (editor, changeObject) {
|
||||
/**
|
||||
* Returns the range that is smaller (i.e. that is before the other in the editor)
|
||||
*/
|
||||
function getLesserRange (from, to) {
|
||||
if (from.line > to.line) {
|
||||
from = to
|
||||
} else {
|
||||
if (from.ch > to.ch) {
|
||||
from = to
|
||||
}
|
||||
}
|
||||
return from
|
||||
}
|
||||
|
||||
function calcTo (from) {
|
||||
let to = {line: from.line, ch: from.ch}
|
||||
const changeArray = changeObject.text || ['']
|
||||
to.line += changeArray.length - 1
|
||||
let charactersInLastLineOfChange = changeArray[changeArray.length - 1].length
|
||||
// If the new text is not empty we need to subtract one from the length due to the counting starting at 0
|
||||
if (changeArray[changeArray.length - 1] !== '') {
|
||||
charactersInLastLineOfChange -= 1
|
||||
}
|
||||
if (from.line === to.line) {
|
||||
to.ch += charactersInLastLineOfChange
|
||||
} else {
|
||||
to.ch = charactersInLastLineOfChange
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
if (dictionary !== null) {
|
||||
let from = getLesserRange(changeObject.from, changeObject.to)
|
||||
let to = calcTo(from)
|
||||
|
||||
if (from.line === to.line && from.ch === to.ch) {
|
||||
if (changeObject.text[changeObject.text.length - 1] !== '') {
|
||||
from.ch -= 1
|
||||
}
|
||||
}
|
||||
const existingMarks = editor.findMarks(from, to) || []
|
||||
for (const mark of existingMarks) {
|
||||
mark.clear()
|
||||
}
|
||||
checkMultiLineRange(this, editor, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DICTIONARY_PATH,
|
||||
CSS_ERROR_CLASS,
|
||||
SPELLCHECK_DISABLED,
|
||||
getAvailableDictionaries,
|
||||
initialize,
|
||||
handleChange,
|
||||
checkRange,
|
||||
checkMultiLineRange,
|
||||
checkWholeDocument,
|
||||
setDictionaryForTestsOnly
|
||||
}
|
||||
Reference in New Issue
Block a user