Merge pull request #130 from vkbo/difflib_spelling

Simple Spell Check
This commit is contained in:
Veronica K. Berglyd Olsen
2019-11-08 09:08:18 +01:00
committed by GitHub
17 changed files with 374717 additions and 75 deletions
+1
View File
@@ -1,3 +1,4 @@
include LICENSE.md
recursive-include nw/assets *
recursive-include nw/graphics *
recursive-include nw/themes *
+5 -4
View File
@@ -47,8 +47,7 @@ For the apt package manager on Debian systems, the following Python3 packages ar
These are optional, but recommended:
* `python3-enchant` for spell checking
* `python3-pycountry` for translating language codes to language names
* `python3-enchant` for better spell checking
* `python3-latexcodec` for escaping unicode characters in LaTeX export
* `python3-pandoc` for additional exports to Word, Open Office, eBooks, etc.
@@ -64,12 +63,14 @@ python3 -m pip install pyqt5
python3 -m pip install appdirs
python3 -m pip install lxml
python3 -m pip install pyenchant
python3 -m pip install pycountry
python3 -m pip install latexcodec
python3 -m pip install pypandoc
```
On Windows,`pyenchant` may cause problems.
I'm looking for a good alternative for spell checking.
If no external spell checking tool is installed, novelWriter will use a basic spell checker based on standard Python package `difflib`.
Currently, only English dictionaries are available, but more can be added to the `nw/assets/dict` folder.
See the RADME.md file in that folder for how to generate more dictionaries.
This option is both slow and limited.
Note: On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from command line.
You can also right click the `novelWriter.py` file, create a shortcut, then right click again, select "Properties" and change the target to your python executable and `novelWriter.py`.
+6 -17
View File
@@ -5,7 +5,7 @@ import sys
if sys.hexversion < 0x030500F0:
print("ERROR: At least Python 3.5 is required")
exit(1)
sys.exit(1)
try:
import PyQt5.QtWidgets
@@ -13,37 +13,26 @@ try:
import PyQt5.QtCore
except:
print("ERROR: Failed to load dependency python3-pyqt5")
exit(1)
sys.exit(1)
try:
import PyQt5.QtSvg
except:
print("ERROR: Failed to load dependency python3-pyqt5.qtsvg")
exit(1)
sys.exit(1)
try:
import lxml
except:
print("ERROR: Failed to load dependency python3-lxml")
exit(1)
sys.exit(1)
try:
import appdirs
except:
print("ERROR: Failed to load dependency python3-appdirs")
exit(1)
spellPack = None
try:
import enchant
spellPack = "enchant"
except:
print("WARNING: No spell check library found.")
print("Please install python3-enchant if you want to use spell checking")
sys.exit(1)
if __name__ == "__main__":
import nw
inArgs = sys.argv[1:]
if spellPack is not None:
inArgs.append("--spell=%s" % spellPack)
nw.main(inArgs)
nw.main(sys.argv[1:])
+1 -6
View File
@@ -89,7 +89,6 @@ def main(sysArgs=None):
"version",
"config=",
"testmode",
"spell=",
"style=",
]
@@ -106,7 +105,7 @@ def main(sysArgs=None):
" -q, --quiet Disable output to command line. Does not affect log file.\n"
" -t, --time Shows time stamp in logging output.\n"
" -l, --logfile= Specify log file.\n"
" --style= Set Qt5 style flag. Defaults to Fusion.\n"
" --style= Set Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n"
" --headless Do not display GUI. Useful for testing scripts.\n"
).format(
@@ -126,7 +125,6 @@ def main(sysArgs=None):
showTime = False
confPath = None
testMode = False
spellTool = None
qtStyle = "Fusion"
# Parse Options
@@ -163,13 +161,10 @@ def main(sysArgs=None):
confPath = inArg
elif inOpt in ("--testmode"):
testMode = True
elif inOpt in ("--spell"):
spellTool = inArg
# Set Config Options
CONFIG.showGUI = not testMode
CONFIG.debugInfo = debugLevel < logging.INFO
CONFIG.spellTool = spellTool
# Set Logging
if showTime: debugStr = timeStr+debugStr
+12
View File
@@ -0,0 +1,12 @@
## Dictionary Conversion
Source: https://ftp.gnu.org/gnu/aspell/dict/0index.html
```
./configure
make
aspell --dict-dir=. -d en dump master > en.dict
aspell --dict-dir=. -d en_GB-ise-w_accents dump master > en_GB.dict
aspell --dict-dir=. -d en_US-w_accents dump master > en_US.dict
```
+254
View File
@@ -0,0 +1,254 @@
This English word list is comes directly from SCOWL 2019.10.06 (up to level 60,
using the speller/make-aspell-dict script, http://wordlist.sourceforge.net/)
and is thus under the same copyright of SCOWL. The affix file (only
included in the aspell6 package) is based on the Ispell one which is
under the same copyright of Ispell. Part of SCOWL is also based on
Ispell thus the Ispell copyright is included with the SCOWL copyright.
The collective work is Copyright 2000-2018 by Kevin Atkinson as well
as any of the copyrights mentioned below:
Copyright 2000-2018 by Kevin Atkinson
Permission to use, copy, modify, distribute and sell these word
lists, the associated scripts, the output created from the scripts,
and its documentation for any purpose is hereby granted without fee,
provided that the above copyright notice appears in all copies and
that both that copyright notice and this permission notice appear in
supporting documentation. Kevin Atkinson makes no representations
about the suitability of this array for any purpose. It is provided
"as is" without express or implied warranty.
Alan Beale <biljir@pobox.com> also deserves special credit as he has,
in addition to providing the 12Dicts package and being a major
contributor to the ENABLE word list, given me an incredible amount of
feedback and created a number of special lists (those found in the
Supplement) in order to help improve the overall quality of SCOWL.
The 10 level includes the 1000 most common English words (according to
the Moby (TM) Words II [MWords] package), a subset of the 1000 most
common words on the Internet (again, according to Moby Words II), and
frequently class 16 from Brian Kelk's "UK English Wordlist
with Frequency Classification".
The MWords package was explicitly placed in the public domain:
The Moby lexicon project is complete and has
been place into the public domain. Use, sell,
rework, excerpt and use in any way on any platform.
Placing this material on internal or public servers is
also encouraged. The compiler is not aware of any
export restrictions so freely distribute world-wide.
You can verify the public domain status by contacting
Grady Ward
3449 Martha Ct.
Arcata, CA 95521-4884
grady@netcom.com
grady@northcoast.com
The "UK English Wordlist With Frequency Classification" is also in the
Public Domain:
Date: Sat, 08 Jul 2000 20:27:21 +0100
From: Brian Kelk <Brian.Kelk@cl.cam.ac.uk>
> I was wondering what the copyright status of your "UK English
> Wordlist With Frequency Classification" word list as it seems to
> be lacking any copyright notice.
There were many many sources in total, but any text marked
"copyright" was avoided. Locally-written documentation was one
source. An earlier version of the list resided in a filespace called
PUBLIC on the University mainframe, because it was considered public
domain.
Date: Tue, 11 Jul 2000 19:31:34 +0100
> So are you saying your word list is also in the public domain?
That is the intention.
The 20 level includes frequency classes 7-15 from Brian's word list.
The 35 level includes frequency classes 2-6 and words appearing in at
least 11 of 12 dictionaries as indicated in the 12Dicts package. All
words from the 12Dicts package have had likely inflections added via
my inflection database.
The 12Dicts package and Supplement is in the Public Domain.
The WordNet database, which was used in the creation of the
Inflections database, is under the following copyright:
This software and database is being provided to you, the LICENSEE,
by Princeton University under the following license. By obtaining,
using and/or copying this software and database, you agree that you
have read, understood, and will comply with these terms and
conditions.:
Permission to use, copy, modify and distribute this software and
database and its documentation for any purpose and without fee or
royalty is hereby granted, provided that you agree to comply with
the following copyright notice and statements, including the
disclaimer, and that the same appear on ALL copies of the software,
database and documentation, including modifications that you make
for internal use or for distribution.
WordNet 1.6 Copyright 1997 by Princeton University. All rights
reserved.
THIS SOFTWARE AND DATABASE IS PROVIDED "AS IS" AND PRINCETON
UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PRINCETON
UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES OF MERCHANT-
ABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE
LICENSED SOFTWARE, DATABASE OR DOCUMENTATION WILL NOT INFRINGE ANY
THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
The name of Princeton University or Princeton may not be used in
advertising or publicity pertaining to distribution of the software
and/or database. Title to copyright in this software, database and
any associated documentation shall at all times remain with
Princeton University and LICENSEE agrees to preserve same.
The 40 level includes words from Alan's 3esl list found in version 4.0
of his 12dicts package. Like his other stuff the 3esl list is also in the
public domain.
The 50 level includes Brian's frequency class 1, words appearing
in at least 5 of 12 of the dictionaries as indicated in the 12Dicts
package, and uppercase words in at least 4 of the previous 12
dictionaries. A decent number of proper names is also included: The
top 1000 male, female, and Last names from the 1990 Census report; a
list of names sent to me by Alan Beale; and a few names that I added
myself. Finally a small list of abbreviations not commonly found in
other word lists is included.
The name files form the Census report is a government document which I
don't think can be copyrighted.
The file special-jargon.50 uses common.lst and word.lst from the
"Unofficial Jargon File Word Lists" which is derived from "The Jargon
File". All of which is in the Public Domain. This file also contain
a few extra UNIX terms which are found in the file "unix-terms" in the
special/ directory.
The 55 level includes words from Alan's 2of4brif list found in version
4.0 of his 12dicts package. Like his other stuff the 2of4brif is also
in the public domain.
The 60 level includes all words appearing in at least 2 of the 12
dictionaries as indicated by the 12Dicts package.
The 70 level includes Brian's frequency class 0 and the 74,550 common
dictionary words from the MWords package. The common dictionary words,
like those from the 12Dicts package, have had all likely inflections
added. The 70 level also included the 5desk list from version 4.0 of
the 12Dics package which is in the public domain.
The 80 level includes the ENABLE word list, all the lists in the
ENABLE supplement package (except for ABLE), the "UK Advanced Cryptics
Dictionary" (UKACD), the list of signature words from the YAWL package,
and the 10,196 places list from the MWords package.
The ENABLE package, mainted by M\Cooper <thegrendel@theriver.com>,
is in the Public Domain:
The ENABLE master word list, WORD.LST, is herewith formally released
into the Public Domain. Anyone is free to use it or distribute it in
any manner they see fit. No fee or registration is required for its
use nor are "contributions" solicited (if you feel you absolutely
must contribute something for your own peace of mind, the authors of
the ENABLE list ask that you make a donation on their behalf to your
favorite charity). This word list is our gift to the Scrabble
community, as an alternate to "official" word lists. Game designers
may feel free to incorporate the WORD.LST into their games. Please
mention the source and credit us as originators of the list. Note
that if you, as a game designer, use the WORD.LST in your product,
you may still copyright and protect your product, but you may *not*
legally copyright or in any way restrict redistribution of the
WORD.LST portion of your product. This *may* under law restrict your
rights to restrict your users' rights, but that is only fair.
UKACD, by J Ross Beresford <ross@bryson.demon.co.uk>, is under the
following copyright:
Copyright (c) J Ross Beresford 1993-1999. All Rights Reserved.
The following restriction is placed on the use of this publication:
if The UK Advanced Cryptics Dictionary is used in a software package
or redistributed in any form, the copyright notice must be
prominently displayed and the text of this document must be included
verbatim.
There are no other restrictions: I would like to see the list
distributed as widely as possible.
The 95 level includes the 354,984 single words, 256,772 compound
words, 4,946 female names and the 3,897 male names, and 21,986 names
from the MWords package, ABLE.LST from the ENABLE Supplement, and some
additional words found in my part-of-speech database that were not
found anywhere else.
Accent information was taken from UKACD.
The VarCon package was used to create the American, British, Canadian,
and Australian word list. It is under the following copyright:
Copyright 2000-2016 by Kevin Atkinson
Permission to use, copy, modify, distribute and sell this array, the
associated software, and its documentation for any purpose is hereby
granted without fee, provided that the above copyright notice appears
in all copies and that both that copyright notice and this permission
notice appear in supporting documentation. Kevin Atkinson makes no
representations about the suitability of this array for any
purpose. It is provided "as is" without express or implied warranty.
Copyright 2016 by Benjamin Titze
Permission to use, copy, modify, distribute and sell this array, the
associated software, and its documentation for any purpose is hereby
granted without fee, provided that the above copyright notice appears
in all copies and that both that copyright notice and this permission
notice appear in supporting documentation. Benjamin Titze makes no
representations about the suitability of this array for any
purpose. It is provided "as is" without express or implied warranty.
Since the original words lists come from the Ispell distribution:
Copyright 1993, Geoff Kuenning, Granada Hills, CA
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. All modifications to the source code must be clearly marked as
such. Binary redistributions based on modified source code
must be clearly marked as modified versions in the documentation
and/or other materials provided with the distribution.
(clause 4 removed with permission from Geoff Kuenning)
5. The name of Geoff Kuenning may not be used to endorse or promote
products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY GEOFF KUENNING AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL GEOFF KUENNING OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
+127250
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+40 -13
View File
@@ -41,7 +41,6 @@ class Config:
self.appHandle = nw.__package__.lower()
self.showGUI = True
self.debugInfo = False
self.spellTool = None
# Set Paths
self.confPath = None
@@ -51,9 +50,10 @@ class Config:
self.appPath = None
self.appRoot = None
self.appIcon = None
self.guiPath = None
self.assetPath = None
self.themeRoot = None
self.themePath = None
self.graphPath = None
self.dictPath = None
# Set default values
self.confChanged = False
@@ -90,10 +90,12 @@ class Config:
self.showTabsNSpaces = False
self.showLineEndings = False
self.fmtApostrophe = nwUnicode.U_RSQUO
self.fmtSingleQuotes = [nwUnicode.U_LSQUO,nwUnicode.U_RSQUO]
self.fmtDoubleQuotes = [nwUnicode.U_LDQUO,nwUnicode.U_RDQUO]
self.spellLanguage = "en_GB"
self.spellTool = None
self.spellLanguage = None
## Backup
self.backupPath = ""
@@ -136,9 +138,9 @@ class Config:
self.osDarwin = False
self.osUnknown = False
if self.osType.startswith("linux"):
self.osLinux = True
self.osLinux = True
elif self.osType.startswith("darwin"):
self.osDarwin = True
self.osDarwin = True
elif self.osType.startswith("win32"):
self.osWindows = True
elif self.osType.startswith("cygwin"):
@@ -146,6 +148,10 @@ class Config:
else:
self.osUnknown = True
# Packages
self.hasEnchant = False
self.hasSymSpell = False
return
##
@@ -165,10 +171,10 @@ class Config:
self.lastPath = self.homePath
self.appPath = getattr(sys, "_MEIPASS", path.abspath(path.dirname(__file__)))
self.appRoot = path.join(self.appPath,path.pardir)
self.helpPath = path.join(self.appRoot,"help","en_GB")
self.guiPath = path.join(self.appPath,"gui")
self.assetPath = path.join(self.appPath,"assets")
self.themeRoot = path.join(self.appPath,"themes")
self.graphPath = path.join(self.appPath,"graphics")
self.dictPath = path.join(self.assetPath,"dict")
self.appIcon = path.join(self.graphPath, nwFiles.APP_ICON)
# If config folder does not exist, make it.
@@ -185,13 +191,16 @@ class Config:
# If it exists, load it
self.loadConfig()
else:
# If it does not exist, save a copy of the defaults
# If it does not exist, save a copy of the default values
self.saveConfig()
# Check the availability of optional packages
self._checkOptionalPackages()
if self.spellTool is None:
logger.warning("No spell check tool available")
else:
logger.debug("Using spell check tool '%s'" % self.spellTool)
self.spellTool = "internal"
if self.spellLanguage is None:
self.spellLanguage = "en"
return True
@@ -287,8 +296,11 @@ class Config:
self.fmtDoubleQuotes = self._parseLine(
cnfParse, cnfSec, "fmtdoublequote", self.CNF_LIST, self.fmtDoubleQuotes
)
self.spellTool = self._parseLine(
cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool
)
self.spellLanguage = self._parseLine(
cnfParse, cnfSec, "spellcheck", self.CNF_STR, self.spellLanguage
cnfParse, cnfSec, "spellcheck", self.CNF_STR, self.spellLanguage
)
self.showTabsNSpaces = self._parseLine(
cnfParse, cnfSec, "showtabsnspaces", self.CNF_BOOL, self.showTabsNSpaces
@@ -379,6 +391,7 @@ class Config:
cnfParse.set(cnfSec,"repdots", str(self.doReplaceDots))
cnfParse.set(cnfSec,"fmtsinglequote", self._packList(self.fmtSingleQuotes))
cnfParse.set(cnfSec,"fmtdoublequote", self._packList(self.fmtDoubleQuotes))
cnfParse.set(cnfSec,"spelltool", str(self.spellTool))
cnfParse.set(cnfSec,"spellcheck", str(self.spellLanguage))
cnfParse.set(cnfSec,"showtabsnspaces", str(self.showTabsNSpaces))
cnfParse.set(cnfSec,"showlineendings", str(self.showLineEndings))
@@ -519,4 +532,18 @@ class Config:
return None
return checkVal
def _checkOptionalPackages(self):
"""Cheks if we have the optional packages used by some features.
"""
try:
import enchant
self.hasEnchant = True
logger.debug("Checking package pyenchant: Ok")
except:
self.hasEnchant = False
logger.debug("Checking package pyenchant: Missing")
return
# End Class Config
+67 -16
View File
@@ -16,8 +16,10 @@ import nw
from os import path
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush, QStandardItemModel, QFont
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtGui import (
QIcon, QPixmap, QColor, QBrush, QStandardItemModel, QFont
)
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QFormLayout, QLineEdit, QPlainTextEdit,
QLabel, QWidget, QTabWidget, QDialogButtonBox, QSpinBox, QGroupBox,
@@ -25,6 +27,7 @@ from PyQt5.QtWidgets import (
QFileDialog
)
from nw.tools import NWSpellCheck, NWSpellSimple, NWSpellEnchant
from nw.constants import nwAlert, nwQuotes
logger = logging.getLogger(__name__)
@@ -156,14 +159,27 @@ class GuiConfigEditGeneral(QWidget):
self.spellLang.setLayout(self.spellLangForm)
self.spellLangList = QComboBox(self)
for spTag, spName in self.theParent.docEditor.theDict.listDictionaries():
self.spellLangList.addItem(spName, spTag)
spellIdx = self.spellLangList.findData(self.mainConf.spellLanguage)
if spellIdx != -1:
self.spellLangList.setCurrentIndex(spellIdx)
self.spellToolList = QComboBox(self)
self.spellToolList.addItem("Internal (difflib)", NWSpellCheck.SP_INTERNAL)
self.spellToolList.addItem("Spell Enchant (pyenchant)", NWSpellCheck.SP_ENCHANT)
self.spellToolList.addItem("SymSpell (symspellpy)", NWSpellCheck.SP_SYMSPELL)
self.spellLangForm.addWidget(QLabel("Language"), 0, 0)
self.spellLangForm.addWidget(self.spellLangList, 0, 1)
theModel = self.spellToolList.model()
idEnchant = self.spellToolList.findData(NWSpellCheck.SP_ENCHANT)
idSymSpell = self.spellToolList.findData(NWSpellCheck.SP_SYMSPELL)
theModel.item(idEnchant).setEnabled(self.mainConf.hasEnchant)
theModel.item(idSymSpell).setEnabled(self.mainConf.hasSymSpell)
self.spellToolList.currentIndexChanged.connect(self._doUpdateSpellTool)
toolIdx = self.spellToolList.findData(self.mainConf.spellTool)
if toolIdx != -1:
self.spellToolList.setCurrentIndex(toolIdx)
self._doUpdateSpellTool(0)
self.spellLangForm.addWidget(QLabel("Provider"), 0, 0)
self.spellLangForm.addWidget(self.spellToolList, 0, 1)
self.spellLangForm.addWidget(QLabel("Language"), 1, 0)
self.spellLangForm.addWidget(self.spellLangList, 1, 1)
self.spellLangForm.setColumnStretch(2, 1)
# AutoSave
@@ -227,8 +243,8 @@ class GuiConfigEditGeneral(QWidget):
self.outerBox.addWidget(self.guiLook, 0, 0)
self.outerBox.addWidget(self.spellLang, 1, 0)
self.outerBox.addWidget(self.autoSave, 2, 0)
self.outerBox.addWidget(self.projBackup, 3, 0)
self.outerBox.setColumnStretch(2, 1)
self.outerBox.addWidget(self.projBackup, 3, 0, 1, 2)
self.outerBox.setColumnStretch(1, 1)
self.outerBox.setRowStretch(4, 1)
self.setLayout(self.outerBox)
@@ -241,6 +257,7 @@ class GuiConfigEditGeneral(QWidget):
guiTheme = self.guiLookTheme.currentData()
guiSyntax = self.guiLookSyntax.currentData()
spellTool = self.spellToolList.currentData()
spellLanguage = self.spellLangList.currentData()
autoSaveDoc = self.autoSaveDoc.value()
autoSaveProj = self.autoSaveProj.value()
@@ -253,6 +270,7 @@ class GuiConfigEditGeneral(QWidget):
self.mainConf.guiTheme = guiTheme
self.mainConf.guiSyntax = guiSyntax
self.mainConf.spellTool = spellTool
self.mainConf.spellLanguage = spellLanguage
self.mainConf.autoSaveDoc = autoSaveDoc
self.mainConf.autoSaveProj = autoSaveProj
@@ -286,6 +304,39 @@ class GuiConfigEditGeneral(QWidget):
return False
def _disableComboItem(self, theList, theValue):
theIdx = theList.findData(theValue)
theModel = theList.model()
anItem = theModel.item(1)
anItem.setFlags(anItem.flags() ^ Qt.ItemIsEnabled)
return theModel
def _doUpdateSpellTool(self, currIdx):
spellTool = self.spellToolList.currentData()
self._updateLanguageList(spellTool)
return
def _updateLanguageList(self, spellTool):
"""Updates the list of available spell checking dictionaries
available for the selected spell check tool. It will try to
preserve the language choice, if the language exists in the
updated list.
"""
if spellTool == NWSpellCheck.SP_ENCHANT:
theDict = NWSpellEnchant()
else:
theDict = NWSpellSimple()
self.spellLangList.clear()
for spTag, spName in theDict.listDictionaries():
self.spellLangList.addItem(spName, spTag)
spellIdx = self.spellLangList.findData(self.mainConf.spellLanguage)
if spellIdx != -1:
self.spellLangList.setCurrentIndex(spellIdx)
return
# END Class GuiConfigEditGeneral
class GuiConfigEditEditor(QWidget):
@@ -536,12 +587,12 @@ class GuiConfigEditEditor(QWidget):
self.mainConf.textMargin = textMargin
self.mainConf.tabWidth = tabWidth
autoSelect = self.autoSelect.isChecked()
doReplace = self.autoReplaceMain.isChecked()
doReplaceSQuote = self.autoReplaceSQ.isChecked()
doReplaceDQuote = self.autoReplaceDQ.isChecked()
doReplaceDash = self.autoReplaceDash.isChecked()
doReplaceDots = self.autoReplaceDash.isChecked()
autoSelect = self.autoSelect.isChecked()
doReplace = self.autoReplaceMain.isChecked()
doReplaceSQuote = self.autoReplaceSQ.isChecked()
doReplaceDQuote = self.autoReplaceDQ.isChecked()
doReplaceDash = self.autoReplaceDash.isChecked()
doReplaceDots = self.autoReplaceDash.isChecked()
self.mainConf.autoSelect = autoSelect
self.mainConf.doReplace = doReplace
+19 -8
View File
@@ -26,7 +26,7 @@ from PyQt5.QtGui import (
from nw.project import NWDoc
from nw.gui.tools import GuiDocHighlighter, WordCounter
from nw.tools import NWSpellCheck
from nw.tools import NWSpellCheck, NWSpellSimple
from nw.constants import nwFiles, nwUnicode, nwDocAction, nwAlert
logger = logging.getLogger(__name__)
@@ -64,14 +64,9 @@ class GuiDocEditor(QTextEdit):
self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.textMargin)
self.qDocument.contentsChange.connect(self._docChange)
if self.mainConf.spellTool == "enchant":
from nw.tools.spellenchant import NWSpellEnchant
self.theDict = NWSpellEnchant()
else:
self.theDict = NWSpellCheck()
# Syntax
self.hLight = GuiDocHighlighter(self.qDocument, self.theParent)
self.hLight.setDict(self.theDict)
# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
@@ -142,7 +137,8 @@ class GuiDocEditor(QTextEdit):
created, and when the user changes the main editor preferences.
"""
# Reload dictionaries
# Reload spell check and dictionaries
self._setupSpellChecking()
self.setDictionaries()
# Set font
@@ -727,4 +723,19 @@ class GuiDocEditor(QTextEdit):
self._findNext()
return
def _setupSpellChecking(self):
"""Create the spell checking object based on the spellTool
setting in config.
"""
if self.mainConf.spellTool == "enchant":
from nw.tools.spellenchant import NWSpellEnchant
self.theDict = NWSpellEnchant()
else:
self.theDict = NWSpellSimple()
self.hLight.setDict(self.theDict)
return
# END Class GuiDocEditor
+2
View File
@@ -4,6 +4,7 @@ from nw.tools.analyse import TextAnalysis
from nw.tools.optlaststate import OptLastState
from nw.tools.spellcheck import NWSpellCheck
from nw.tools.spellenchant import NWSpellEnchant
from nw.tools.spellsimple import NWSpellSimple
from nw.tools.translate import numberToWord
from nw.tools.wordcount import countWords
@@ -12,6 +13,7 @@ __all__ = [
"OptLastState",
"NWSpellCheck",
"NWSpellEnchant",
"NWSpellSimple",
"numberToWord",
"countWords",
]
+38
View File
@@ -17,9 +17,16 @@ logger = logging.getLogger(__name__)
class NWSpellCheck():
SP_INTERNAL = "internal"
SP_ENCHANT = "enchant"
SP_SYMSPELL = "symspell"
theDict = None
PROJW = []
def __init__(self):
self.mainConf = nw.CONFIG
self.projectDict = None
return
def setLanguage(self, theLang, projectDict=None):
@@ -32,9 +39,40 @@ class NWSpellCheck():
return []
def addWord(self, newWord):
if self.projectDict is not None and newWord not in self.PROJW:
newWord = newWord.strip()
self.PROJW.append(newWord)
try:
with open(self.projectDict,mode="w+",encoding="utf-8") as outFile:
for pWord in self.PROJW:
outFile.write("%s\n" % pWord)
except Exception as e:
logger.error("Failed to write to project word list at %s" % str(self.projectDict))
logger.error(str(e))
return
def listDictionaries(self):
return []
##
# Internal Functions
##
def _readProjectDictionary(self, projectDict):
self.PROJW = []
if projectDict is not None:
self.projectDict = projectDict
try:
with open(projectDict,mode="r",encoding="utf-8") as wordsFile:
for theLine in wordsFile:
theLine = theLine.strip()
if len(theLine) > 0 and theLine not in self.PROJW:
self.PROJW.append(theLine)
logger.debug("Project word list")
logger.debug("Project word list contains %d words" % len(self.PROJW))
except Exception as e:
logger.error("Failed to load project word list")
logger.error(str(e))
return
# END Class NWSpellCheck
+10 -9
View File
@@ -32,15 +32,16 @@ class NWSpellEnchant(NWSpellCheck):
crash.
"""
try:
if projectDict is None:
self.theDict = enchant.Dict(theLang)
else:
self.theDict = enchant.DictWithPWL(theLang, projectDict)
self.theDict = enchant.Dict(theLang)
logger.debug("Enchant spell checking for language %s loaded" % theLang)
except:
logger.error("Failed to load enchant spell checking for language %s" % theLang)
self.theDict = NWSpellEnchantDummy()
self._readProjectDictionary(projectDict)
for pWord in self.PROJW:
self.theDict.add_to_session(pWord)
return
def checkWord(self, theWord):
@@ -50,7 +51,8 @@ class NWSpellEnchant(NWSpellCheck):
return self.theDict.suggest(theWord)
def addWord(self, newWord):
self.theDict.add_to_pwl(newWord)
self.theDict.add_to_session(newWord)
NWSpellCheck.addWord(self, newWord)
return
def listDictionaries(self):
@@ -58,10 +60,9 @@ class NWSpellEnchant(NWSpellCheck):
for spTag, spProvider in enchant.list_dicts():
spList = []
if spTag[:2] in isoLanguage.ISO_639_1:
langName = isoLanguage.ISO_639_1[spTag[:2]]
spList.append(isoLanguage.ISO_639_1[spTag[:2]])
else:
langName = spTag[:2]
spList.append(langName)
spList.append(spTag[:2])
if len(spTag) > 3:
spList.append("(%s)" % spTag[3:])
spList.append("[%s]" % spProvider.name)
@@ -82,7 +83,7 @@ class NWSpellEnchantDummy:
def suggest(self, theWord):
return []
def add_to_pwl(self, theWord):
def add_to_session(self, theWord):
return
# END Class NWSpellEnchantDummy
+122
View File
@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""novelWriter Spell Check Simple
novelWriter Spell Check Simple
==================================
Simple spell checker based on difflib
File History:
Created: 2019-06-11 [0.1.5]
"""
import logging
import nw
from os import path, listdir
from difflib import get_close_matches
logger = logging.getLogger(__name__)
from nw.tools.spellcheck import NWSpellCheck
from nw.constants import isoLanguage
class NWSpellSimple(NWSpellCheck):
WORDS = []
def __init__(self):
NWSpellCheck.__init__(self)
logger.debug("Simple spell checking activated")
return
def setLanguage(self, theLang, projectDict=None):
self.WORDS = []
dictFile = path.join(self.mainConf.dictPath,theLang+".dict")
try:
with open(dictFile,mode="r",encoding="utf-8") as wordsFile:
for theLine in wordsFile:
if len(theLine) == 0 or theLine.startswith("#"):
continue
self.WORDS.append(theLine.strip().lower())
logger.debug("Spell check word list for language %s loaded" % theLang)
logger.debug("Word list contains %d words" % len(self.WORDS))
except Exception as e:
logger.error("Failed to load spell check word list for language %s" % theLang)
logger.error(str(e))
self._readProjectDictionary(projectDict)
for pWord in self.PROJW:
if pWord not in self.WORDS:
self.WORDS.append(pWord)
return
def checkWord(self, theWord):
"""Check if a word exists in the word list. Make sure to keep
this function as fast as possible as it is called for every
word by the syntax highlighter.
"""
theWord = theWord.replace(self.mainConf.fmtApostrophe,"'").lower()
return theWord in self.WORDS
def suggestWords(self, theWord):
"""Get suggestions for correct word from difflib, and make sure
the first character is upper case if that was also the case for
the word be3ing checked. Also make sure the apostrophe is
changed to the one in the dictionary, and then put back in the
results.
"""
theWord = theWord.strip()
if len(theWord) == 0:
return []
firstUp = theWord[0] == theWord[0].upper()
theWord = theWord.lower()
theMatches = get_close_matches(theWord, self.WORDS, n=10, cutoff=0.75)
theOptions = []
for aWord in theMatches:
if len(aWord) == 0:
continue
if firstUp:
aWord = aWord[0].upper() + aWord[1:]
aWord = aWord.replace("'",self.mainConf.fmtApostrophe)
theOptions.append(aWord)
return theOptions
def addWord(self, newWord):
newWord = newWord.strip().lower()
if newWord not in self.WORDS:
self.WORDS.append(newWord)
NWSpellCheck.addWord(self, newWord)
return
def listDictionaries(self):
retList = []
for dictFile in listdir(self.mainConf.dictPath):
theBits = path.splitext(dictFile)
if len(theBits) != 2:
continue
if theBits[1] != ".dict":
continue
spTag = theBits[0]
spList = []
if spTag[:2] in isoLanguage.ISO_639_1:
spList.append(isoLanguage.ISO_639_1[spTag[:2]])
else:
spList.append(spTag[:2])
if len(spTag) > 3:
spList.append("(%s)" % spTag[3:])
spList.append("[internal]")
spName = " ".join(spList)
retList.append((spTag, spName))
return retList
# END Class NWSpellSimple
+3 -2
View File
@@ -1,5 +1,5 @@
[Main]
timestamp = 2019-11-03 00:40:02
timestamp = 2019-11-08 00:27:54
theme = default
syntax = default_light
@@ -29,7 +29,8 @@ repdash = True
repdots = True
fmtsinglequote = ,
fmtdoublequote = “, ”
spellcheck = en_GB
spelltool = internal
spellcheck = en
showtabsnspaces = False
showlineendings = False