From 3c2864f161a6fb4fd1425b56ce3bb579bc5c6bb4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Tue, 11 Jun 2019 18:46:43 +0200 Subject: [PATCH 1/4] Added spell checker class --- nw/tools/spellcheck.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 nw/tools/spellcheck.py diff --git a/nw/tools/spellcheck.py b/nw/tools/spellcheck.py new file mode 100644 index 00000000..17f4f59c --- /dev/null +++ b/nw/tools/spellcheck.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +"""novelWriter Spell Check Wrapper + + novelWriter – Spell Check Wrapper +=================================== + Wrapper class for spell checking + + File History: + Created: 2019-06-11 [0.1.5] + +""" + +import logging +import enchant +import nw + +logger = logging.getLogger(__name__) + +class NWSpellCheck(): + +# END Class NWSpellCheck From c48b1deb8f4f4916c1c974f56d664e1b9f9e4ede Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Tue, 11 Jun 2019 20:27:29 +0200 Subject: [PATCH 2/4] Added wrapper classes for spell checker so new libraries can be added --- novelWriter.py | 11 ++++++--- nw/__init__.py | 5 ++++ nw/config.py | 6 +++++ nw/convert/tohtml.py | 6 ++--- nw/convert/tokenizer.py | 8 +++---- nw/gui/configeditor.py | 5 ++-- nw/gui/doceditor.py | 19 +++++++++------ nw/gui/dochighlight.py | 2 +- nw/tools/spellcheck.py | 20 ++++++++++++++++ nw/tools/spellenchant.py | 52 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 nw/tools/spellenchant.py diff --git a/novelWriter.py b/novelWriter.py index ea769c11..76b04378 100755 --- a/novelWriter.py +++ b/novelWriter.py @@ -29,12 +29,17 @@ except: print("ERROR: Failed to load dependency python3-appdirs") exit(1) +spellPack = None try: import enchant + spellPack = "enchant" except: - print("ERROR: Failed to load dependency python3-enchant") - exit(1) + print("WARNING: No spell check library found.") + print("Please install python3-enchant if you want to use spell checking") if __name__ == "__main__": import nw - nw.main(sys.argv[1:]) + inArgs = sys.argv[1:] + if spellPack is not None: + inArgs.append("--spell=%s" % spellPack) + nw.main(inArgs) diff --git a/nw/__init__.py b/nw/__init__.py index 2c78b458..1c43ad9a 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -82,6 +82,7 @@ def main(sysArgs): "version", "config=", "testmode", + "spell=", ] helpMsg = ( @@ -117,6 +118,7 @@ def main(sysArgs): confPath = None testMode = False debugGUI = False + spellTool = None # Parse Options try: @@ -149,6 +151,8 @@ def main(sysArgs): confPath = inArg elif inOpt in ("--testmode"): testMode = True + elif inOpt in ("--spell"): + spellTool = inArg elif inOpt in ("-D","--debuggui"): debugLevel = logging.DEBUG debugStr = "{name:>20}:{lineno:<4d} {levelname:8} {message:}" @@ -158,6 +162,7 @@ def main(sysArgs): CONFIG.showGUI = not testMode CONFIG.debugGUI = debugGUI CONFIG.debugInfo = debugLevel < logging.INFO + CONFIG.spellTool = spellTool # Set Logging if showTime: debugStr = timeStr+debugStr diff --git a/nw/config.py b/nw/config.py index e7da0533..2c926614 100644 --- a/nw/config.py +++ b/nw/config.py @@ -35,6 +35,7 @@ class Config: self.showGUI = True self.debugGUI = False self.debugInfo = False + self.spellTool = None # Set Paths self.confPath = None @@ -124,6 +125,11 @@ class Config: # If it does not exist, save a copy of the defaults self.saveConfig() + if self.spellTool is None: + logger.warning("No spell check tool available") + else: + logger.debug("Using spell check tool '%s'" % self.spellTool) + return True def loadConfig(self): diff --git a/nw/convert/tohtml.py b/nw/convert/tohtml.py index 3225c4bb..c6ec8975 100644 --- a/nw/convert/tohtml.py +++ b/nw/convert/tohtml.py @@ -28,13 +28,13 @@ class ToHtml(Tokenizer): def doAutoReplace(self): Tokenizer.doAutoReplace(self) - theDict = { + repDict = { "<" : "<", ">" : ">", "&" : "&", } - xRep = re.compile("|".join([re.escape(k) for k in theDict.keys()]), flags=re.DOTALL) - self.theText = xRep.sub(lambda x: theDict[x.group(0)], self.theText) + xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL) + self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText) return diff --git a/nw/convert/tokenizer.py b/nw/convert/tokenizer.py index 59365ec1..0d62175f 100644 --- a/nw/convert/tokenizer.py +++ b/nw/convert/tokenizer.py @@ -60,11 +60,11 @@ class Tokenizer(): def doAutoReplace(self): if len(self.theProject.autoReplace) > 0: - theDict = {} + repDict = {} for aKey, aVal in self.theProject.autoReplace.items(): - theDict["<%s>" % aKey] = aVal - xRep = re.compile("|".join([re.escape(k) for k in theDict.keys()]), flags=re.DOTALL) - self.theText = xRep.sub(lambda x: theDict[x.group(0)], self.theText) + repDict["<%s>" % aKey] = aVal + xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL) + self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText) return def tokenizeText(self): diff --git a/nw/gui/configeditor.py b/nw/gui/configeditor.py index fa1d63f4..116434de 100644 --- a/nw/gui/configeditor.py +++ b/nw/gui/configeditor.py @@ -11,7 +11,6 @@ """ import logging -import enchant import nw from os import path @@ -139,8 +138,8 @@ class GuiConfigEditGeneral(QWidget): self.spellLang.setLayout(self.spellLangForm) self.spellLangList = QComboBox(self) - for spTag, spProvider in enchant.list_dicts(): - self.spellLangList.addItem("%s [%s]" % (spTag, spProvider.name), spTag) + 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) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 9b36790a..1100c419 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -12,7 +12,6 @@ import logging import nw -import enchant from time import time @@ -23,6 +22,7 @@ from PyQt5.QtGui import QTextCursor, QTextOption, QIcon, QKeySequence, Q from nw.project.document import NWDoc from nw.gui.dochighlight import GuiDocHighlighter from nw.gui.wordcounter import WordCounter +from nw.tools.spellcheck import NWSpellCheck from nw.enum import nwDocAction, nwAlert logger = logging.getLogger(__name__) @@ -58,7 +58,13 @@ class GuiDocEditor(QTextEdit): # Core Elements self.theQDoc = self.document() - self.theDict = enchant.Dict(self.mainConf.spellLanguage) + if self.mainConf.spellTool == "enchant": + from nw.tools.spellenchant import NWSpellEnchant + self.theDict = NWSpellEnchant() + else: + self.theDict = NWSpellCheck() + + self.theDict.setLanguage(self.mainConf.spellLanguage) self.hLight = GuiDocHighlighter(self.theQDoc, self.theParent) self.hLight.setDict(self.theDict) @@ -215,8 +221,7 @@ class GuiDocEditor(QTextEdit): def setPwl(self, pwlFile): if pwlFile is not None: self.pwlFile = pwlFile - self.theDict = enchant.DictWithPWL(self.mainConf.spellLanguage,pwlFile) - self.hLight.setDict(self.theDict) + self.theDict.setLanguage(self.mainConf.spellLanguage, pwlFile) return True def setSpellCheck(self, theMode): @@ -314,7 +319,7 @@ class GuiDocEditor(QTextEdit): theWord = theCursor.selectedText() if theWord == "": return - if self.theDict.check(theWord): + if self.theDict.checkWord(theWord): return mnuSuggest = QMenu() @@ -322,7 +327,7 @@ class GuiDocEditor(QTextEdit): mnuHead = QAction(spIcon,"Spelling Suggestion", mnuSuggest) mnuSuggest.addAction(mnuHead) mnuSuggest.addSeparator() - theSuggest = self.theDict.suggest(theWord) + theSuggest = self.theDict.suggestWords(theWord) if len(theSuggest) > 0: for aWord in theSuggest: mnuWord = QAction(aWord, mnuSuggest) @@ -353,7 +358,7 @@ class GuiDocEditor(QTextEdit): def _addWord(self, theCursor): theWord = theCursor.selectedText().strip() logger.info("Added '%s' to project dictionary" % theWord) - self.theDict.add_to_pwl(theWord) + self.theDict.addWord(theWord) self.hLight.setDict(self.theDict) self.hLight.rehighlightBlock(theCursor.block()) return diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 55ae8b05..97fb1e89 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -234,7 +234,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): rxSpell = self.spellRx.globalMatch(theText.replace("_"," "), 0) while rxSpell.hasNext(): rxMatch = rxSpell.next() - if not self.theDict.check(rxMatch.captured(0)): + if not self.theDict.checkWord(rxMatch.captured(0)): if rxMatch.captured(0) == rxMatch.captured(0).upper(): continue xPos = rxMatch.capturedStart(0) diff --git a/nw/tools/spellcheck.py b/nw/tools/spellcheck.py index 17f4f59c..95ebbc60 100644 --- a/nw/tools/spellcheck.py +++ b/nw/tools/spellcheck.py @@ -18,4 +18,24 @@ logger = logging.getLogger(__name__) class NWSpellCheck(): + theDict = None + + def __init__(self): + return + + def setLanguage(self, theLang, projectDict=None): + return + + def checkWord(self, theWord): + return True + + def suggestWords(self, theWord): + return [] + + def addWord(self, newWord): + return + + def listDictionaries(self): + return [] + # END Class NWSpellCheck diff --git a/nw/tools/spellenchant.py b/nw/tools/spellenchant.py new file mode 100644 index 00000000..afa6a3cb --- /dev/null +++ b/nw/tools/spellenchant.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +"""novelWriter Spell Check Wrapper : pyEnchant + + novelWriter – Spell Check Wrapper : pyEnchant +=============================================== + Wrapper class for spell checking with pyEnchant + + File History: + Created: 2019-06-11 [0.1.5] + +""" + +import logging +import enchant +import nw + +from nw.tools.spellcheck import NWSpellCheck + +logger = logging.getLogger(__name__) + +class NWSpellEnchant(NWSpellCheck): + + def __init__(self): + NWSpellCheck.__init__(self) + logger.debug("Enchant spell checking activated") + return + + def setLanguage(self, theLang, projectDict=None): + if projectDict is None: + self.theDict = enchant.Dict(theLang) + else: + self.theDict = enchant.DictWithPWL(theLang, projectDict) + logger.debug("Enchant spell checking for %s loaded" % theLang) + return + + def checkWord(self, theWord): + return self.theDict.check(theWord) + + def suggestWords(self, theWord): + return self.theDict.suggest(theWord) + + def addWord(self, newWord): + self.theDict.add_to_pwl(newWord) + return + + def listDictionaries(self): + retList = [] + for spTag, spProvider in enchant.list_dicts(): + retList.append((spTag, "%s [%s]" % (spTag, spProvider.name))) + return retList + +# END Class NWSpellEnchant From 59ba847c0fb8d7dba6b2bd7c2c274fda4b34fe85 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Tue, 11 Jun 2019 20:52:24 +0200 Subject: [PATCH 3/4] Completed the pyenchant wrapper class --- README.md | 14 +++++++++----- nw/gui/winmain.py | 2 +- nw/tools/spellenchant.py | 23 +++++++++++++++++++++-- requirements.txt | 1 + sample/sampleNovel/meta/sessionInfo.log | 7 +++++++ sample/sampleNovel/nwProject.nwx | 4 ++-- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8a58806f..6b8a73e8 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,16 @@ Most shortcuts are labelled with the corresponding dropdown menu entry. ## Dependencies -For the apt package manager, the following Python3 packages are needed. +For the apt package manager, the following Python3 packages are needed: -* python3-pyqt5 -* python3-appdirs -* python3-lxml -* python3-enchant +* `python3-pyqt5` for the GUI +* `python3-appdirs` for locating the system's config folder +* `python3-lxml` for writing project files + +These are optional, but recommended: + +* `python3-enchant` for spell checking +* `python3-pycountry` for translating language codes to language names Alternatively, the packages can be installed with `pip` by running ``` diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index 0c8e8d76..0900cf11 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -520,7 +520,7 @@ class GuiMain(QMainWindow): def closeMain(self): - if self.mainConf.showGUI: + if self.mainConf.showGUI and self.hasProject: msgBox = QMessageBox() msgRes = msgBox.question( self, "Exit", "Do you want to save changes and exit?" diff --git a/nw/tools/spellenchant.py b/nw/tools/spellenchant.py index afa6a3cb..61c9a012 100644 --- a/nw/tools/spellenchant.py +++ b/nw/tools/spellenchant.py @@ -14,6 +14,12 @@ import logging import enchant import nw +try: + import pycountry + hasPyCountry = True +except: + hasPyCountry = False + from nw.tools.spellcheck import NWSpellCheck logger = logging.getLogger(__name__) @@ -30,7 +36,7 @@ class NWSpellEnchant(NWSpellCheck): self.theDict = enchant.Dict(theLang) else: self.theDict = enchant.DictWithPWL(theLang, projectDict) - logger.debug("Enchant spell checking for %s loaded" % theLang) + logger.debug("Enchant spell checking for language %s loaded" % theLang) return def checkWord(self, theWord): @@ -46,7 +52,20 @@ class NWSpellEnchant(NWSpellCheck): def listDictionaries(self): retList = [] for spTag, spProvider in enchant.list_dicts(): - retList.append((spTag, "%s [%s]" % (spTag, spProvider.name))) + if hasPyCountry: + spList = [] + try: + langObj = pycountry.languages.get(alpha_2 = spTag[:2]) + spList.append(langObj.name) + except: + spList.append(spTag[:2]) + if len(spTag) > 3: + spList.append("(%s)" % spTag[3:]) + spList.append("[%s]" % spProvider.name) + spName = " ".join(spList) + else: + spName = "%s [%s]" % (spTag, spProvider.name) + retList.append((spTag, spName)) return retList # END Class NWSpellEnchant diff --git a/requirements.txt b/requirements.txt index 039ca559..a83517dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ pyqt5 appdirs lxml pyenchant +pycountry diff --git a/sample/sampleNovel/meta/sessionInfo.log b/sample/sampleNovel/meta/sessionInfo.log index 1defe611..3a0f392b 100644 --- a/sample/sampleNovel/meta/sessionInfo.log +++ b/sample/sampleNovel/meta/sessionInfo.log @@ -210,3 +210,10 @@ Start: 2019-06-10 21:48:51 End: 2019-06-10 21:49:07 Words: 0 Start: 2019-06-10 22:17:58 End: 2019-06-10 22:18:44 Words: 0 Start: 2019-06-10 22:46:35 End: 2019-06-10 22:49:34 Words: 0 Start: 2019-06-11 14:44:55 End: 2019-06-11 14:53:47 Words: 0 +Start: 2019-06-11 19:26:07 End: 2019-06-11 19:26:18 Words: 0 +Start: 2019-06-11 19:26:44 End: 2019-06-11 19:26:50 Words: 0 +Start: 2019-06-11 19:30:30 End: 2019-06-11 19:30:37 Words: 0 +Start: 2019-06-11 19:31:16 End: 2019-06-11 19:31:50 Words: 0 +Start: 2019-06-11 20:04:39 End: 2019-06-11 20:05:00 Words: 0 +Start: 2019-06-11 20:05:04 End: 2019-06-11 20:08:49 Words: 0 +Start: 2019-06-11 20:20:52 End: 2019-06-11 20:21:18 Words: 0 diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index b7bbbada..8f8715df 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -80,7 +80,7 @@ 736 137 6 - 738 + 473 New File From 2cff09daf2b70b1f945abdff9e4b771d04f75fb4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Tue, 11 Jun 2019 20:53:13 +0200 Subject: [PATCH 4/4] Removed import enchant in dummy class --- nw/tools/spellcheck.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nw/tools/spellcheck.py b/nw/tools/spellcheck.py index 95ebbc60..051767d8 100644 --- a/nw/tools/spellcheck.py +++ b/nw/tools/spellcheck.py @@ -11,7 +11,6 @@ """ import logging -import enchant import nw logger = logging.getLogger(__name__)