diff --git a/nw/tools/analyse.py b/lib/textanalyse.py similarity index 100% rename from nw/tools/analyse.py rename to lib/textanalyse.py diff --git a/nw/additions/__init__.py b/nw/additions/__init__.py deleted file mode 100644 index 213df1b1..00000000 --- a/nw/additions/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -from nw.additions.qconfiglayout import QConfigLayout -from nw.additions.qswitch import QSwitch - -__all__ = [ - "QConfigLayout", - "QSwitch", -] diff --git a/nw/convert/tokenizer.py b/nw/convert/tokenizer.py index f891a9f7..4ac99377 100644 --- a/nw/convert/tokenizer.py +++ b/nw/convert/tokenizer.py @@ -32,8 +32,8 @@ import nw from operator import itemgetter from PyQt5.QtCore import QRegularExpression -from nw.project.document import NWDoc -from nw.tools.translate import numberToWord +from nw.core.document import NWDoc +from nw.core.tools import numberToWord from nw.constants import nwItemLayout logger = logging.getLogger(__name__) diff --git a/nw/core/__init__.py b/nw/core/__init__.py new file mode 100644 index 00000000..a608acd7 --- /dev/null +++ b/nw/core/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from nw.core.document import NWDoc +from nw.core.index import NWIndex +from nw.core.project import NWProject +from nw.core.spellcheck import NWSpellCheck +from nw.core.spellcheck import NWSpellEnchant +from nw.core.spellcheck import NWSpellSimple +from nw.core.tools import countWords +from nw.core.tools import projectMaintenance +from nw.core.tools import numberToWord + +__all__ = [ + "NWDoc", + "NWIndex", + "NWProject", + "NWSpellCheck", + "NWSpellEnchant", + "NWSpellSimple", + "countWords", + "projectMaintenance", + "numberToWord", +] diff --git a/nw/project/document.py b/nw/core/document.py similarity index 100% rename from nw/project/document.py rename to nw/core/document.py diff --git a/nw/project/index.py b/nw/core/index.py similarity index 99% rename from nw/project/index.py rename to nw/core/index.py index 9cc1098b..617ddb65 100644 --- a/nw/project/index.py +++ b/nw/core/index.py @@ -35,7 +35,7 @@ from time import time from nw.constants import ( nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert ) -from nw.tools import countWords +from nw.core.tools import countWords logger = logging.getLogger(__name__) diff --git a/nw/project/project.py b/nw/core/project.py similarity index 99% rename from nw/project/project.py rename to nw/core/project.py index fb4f340a..ef6558cf 100644 --- a/nw/project/project.py +++ b/nw/core/project.py @@ -40,7 +40,8 @@ from datetime import datetime from time import time from shutil import make_archive -from nw.tools import projectMaintenance, OptionState +from nw.gui.tools import OptionState +from nw.core.tools import projectMaintenance from nw.common import checkString, checkBool, checkInt from nw.constants import ( nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py new file mode 100644 index 00000000..fda4022d --- /dev/null +++ b/nw/core/spellcheck.py @@ -0,0 +1,284 @@ +# -*- 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] + + This file is a part of novelWriter + Copyright 2020, Veronica Berglyd Olsen + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import logging +import nw + +from os import path, listdir +from difflib import get_close_matches + +from nw.constants import isoLanguage + +logger = logging.getLogger(__name__) + +# ================================================================================================ # +# SpellChecking SuperClass +# ================================================================================================ # + +class NWSpellCheck(): + + SP_INTERNAL = "internal" + SP_ENCHANT = "enchant" + SP_SYMSPELL = "symspell" + + theDict = None + PROJW = [] + + def __init__(self): + self.mainConf = nw.CONFIG + self.projectDict = None + self.spellLanguage = None + return + + def setLanguage(self, theLang, projectDict=None): + return + + def checkWord(self, theWord): + return True + + def suggestWords(self, theWord): + 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="a+",encoding="utf-8") as outFile: + outFile.write("%s\n" % newWord) + except Exception as e: + logger.error("Failed to add word to project word list %s" % str(self.projectDict)) + logger.error(str(e)) + return + + def listDictionaries(self): + return [] + + @staticmethod + def expandLanguage(spTag): + spBits = spTag.split("_") + if spBits[0] in isoLanguage.ISO_639_1: + spLang = isoLanguage.ISO_639_1[spBits[0]] + else: + spLang = spBits[0] + if len(spBits) > 1: + spLang += " (%s)" % spBits[1] + return spLang + + ## + # Internal Functions + ## + + def _readProjectDictionary(self, projectDict): + self.PROJW = [] + if projectDict is not None: + self.projectDict = projectDict + if not path.isfile(projectDict): + return + 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 + +# ================================================================================================ # +# Enchant Based SpellChecking +# ================================================================================================ # + +class NWSpellEnchant(NWSpellCheck): + + def __init__(self): + NWSpellCheck.__init__(self) + logger.debug("Enchant spell checking activated") + return + + def setLanguage(self, theLang, projectDict=None): + """Load a dictionary for the language specified in the config. + If that fails, we load a dummy dictionary so that lookups don't + crash. + """ + try: + import enchant + self.theDict = enchant.Dict(theLang) + self.spellLanguage = 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.spellLanguage = None + + self._readProjectDictionary(projectDict) + for pWord in self.PROJW: + self.theDict.add_to_session(pWord) + + 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_session(newWord) + NWSpellCheck.addWord(self, newWord) + return + + def listDictionaries(self): + retList = [] + try: + import enchant + for spTag, spProvider in enchant.list_dicts(): + spName = "%s [%s]" % (self.expandLanguage(spTag), spProvider.name) + retList.append((spTag, spName)) + except: + logger.error("Failed to list languages for enchant spell checking") + return retList + +# END Class NWSpellEnchant + +class NWSpellEnchantDummy: + + def __init__(self): + return + + def check(self, theWord): + return True + + def suggest(self, theWord): + return [] + + def add_to_session(self, theWord): + return + +# END Class NWSpellEnchantDummy + +# ================================================================================================ # +# Fallback SpellChecking Using difflib +# ================================================================================================ # + +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)) + self.spellLanguage = theLang + except Exception as e: + logger.error("Failed to load spell check word list for language %s" % theLang) + logger.error(str(e)) + self.spellLanguage = None + + 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 + + spName = "%s [Internal]" % self.expandLanguage(theBits[0]) + retList.append((theBits[0], spName)) + + return retList + +# END Class NWSpellSimple diff --git a/nw/core/tools.py b/nw/core/tools.py new file mode 100644 index 00000000..2c96cee6 --- /dev/null +++ b/nw/core/tools.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +"""novelWriter Word Counter + + novelWriter – Word Counter +============================ + Simple word counter + + File History: + Created: 2019-04-22 [0.0.1] countWords + Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN + Created: 2020-02-13 [0.4.3] projectMaintenance + Merged: 2020-05-08 [0.4.5] All of the above into this file + + This file is a part of novelWriter + Copyright 2020, Veronica Berglyd Olsen + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import logging +import nw + +from os import path, unlink, rmdir + +logger = logging.getLogger(__name__) + +def countWords(theText): + """Count words in a piece of text, skipping special syntax and + comments. + """ + + charCount = 0 + wordCount = 0 + paraCount = 0 + prevEmpty = True + + for aLine in theText.splitlines(): + + countPara = True + theLen = len(aLine) + + if theLen == 0: + prevEmpty = True + continue + if aLine[0] == "@" or aLine[0] == "%": + continue + + if aLine[0:5] == "#### ": + wordCount -= 1 + charCount -= 5 + countPara = False + elif aLine[0:4] == "### ": + wordCount -= 1 + charCount -= 4 + countPara = False + elif aLine[0:3] == "## ": + wordCount -= 1 + charCount -= 3 + countPara = False + elif aLine[0:2] == "# ": + wordCount -= 1 + charCount -= 2 + countPara = False + + theBuff = aLine.replace("–"," ").replace("—"," ") + wordCount += len(theBuff.split()) + charCount += theLen + if countPara and prevEmpty: + paraCount += 1 + prevEmpty = countPara == False + + return charCount, wordCount, paraCount + +def projectMaintenance(theProject): + """Wrapper class for handling various tasks related to managing old + projects with content from older versions of novelWriter. + """ + + # Remove no longer used project cache folder + if path.isdir(theProject.projPath): + cacheDir = path.join(theProject.projPath, "cache") + if path.isdir(cacheDir): + logger.info("Deprecated cache folder found") + rmList = [] + for i in range(10): + rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i)) + rmList.append(path.join(cacheDir, "projCount.txt")) + for rmFile in rmList: + if path.isfile(rmFile): + logger.info("Deleting: %s" % rmFile) + try: + unlink(rmFile) + except Exception as e: + logger.error(str(e)) + logger.info("Deleting: %s" % cacheDir) + try: + rmdir(cacheDir) + except Exception as e: + logger.error(str(e)) + + # Remove no longer used meta files + rmList = [] + rmList.append(path.join(theProject.projMeta, "mainOptions.json")) + rmList.append(path.join(theProject.projMeta, "exportOptions.json")) + rmList.append(path.join(theProject.projMeta, "outlineOptions.json")) + rmList.append(path.join(theProject.projMeta, "timelineOptions.json")) + rmList.append(path.join(theProject.projMeta, "docMergeOptions.json")) + rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json")) + for rmFile in rmList: + if path.isfile(rmFile): + logger.info("Deleting: %s" % rmFile) + try: + unlink(rmFile) + except Exception as e: + logger.error(str(e)) + + return + +def numberToWord(numVal, theLanguage): + """Wrapper for converting numbers to words for chapter headings. + """ + numWord = "" + if theLanguage == "en": + numWord = _numberToWordEN(numVal) + else: + numWord = _numberToWordEN(numVal) + # print("%4d : %s" % (numVal, numWord)) + return numWord + +def _numberToWordEN(numVal): + """Convert numbers to English words. + """ + + numWord = "" + oneWord = "" + tenWord = "" + hunWord = "" + + if numVal == 0: + return "Zero" + + oneVal = numVal % 10 + tenVal = (numVal-oneVal) % 100 + hunVal = (numVal-tenVal-oneVal) % 1000 + + if hunVal == 100: hunWord = "One Hundred" + if hunVal == 200: hunWord = "Two Hundred" + if hunVal == 300: hunWord = "Three Hundred" + if hunVal == 400: hunWord = "Four Hundred" + if hunVal == 500: hunWord = "Five Hundred" + if hunVal == 600: hunWord = "Six Hundred" + if hunVal == 700: hunWord = "Seven Hundred" + if hunVal == 800: hunWord = "Eight Hundred" + if hunVal == 900: hunWord = "Nine Hundred" + + if tenVal == 20: tenWord = "Twenty" + if tenVal == 30: tenWord = "Thirty" + if tenVal == 40: tenWord = "Forty" + if tenVal == 50: tenWord = "Fifty" + if tenVal == 60: tenWord = "Sixty" + if tenVal == 70: tenWord = "Seventy" + if tenVal == 80: tenWord = "Eighty" + if tenVal == 90: tenWord = "Ninety" + + if tenVal == 10: + if oneVal == 0: oneWord = "Ten" + if oneVal == 1: oneWord = "Eleven" + if oneVal == 2: oneWord = "Twelve" + if oneVal == 3: oneWord = "Thirteen" + if oneVal == 4: oneWord = "Fourteen" + if oneVal == 5: oneWord = "Fifteen" + if oneVal == 6: oneWord = "Sixteen" + if oneVal == 7: oneWord = "Seventeen" + if oneVal == 8: oneWord = "Eighteen" + if oneVal == 9: oneWord = "Nineteen" + numWord = ("%s %s" % (hunWord, oneWord)).strip() + else: + if oneVal == 0: oneWord = "" + if oneVal == 1: oneWord = "One" + if oneVal == 2: oneWord = "Two" + if oneVal == 3: oneWord = "Three" + if oneVal == 4: oneWord = "Four" + if oneVal == 5: oneWord = "Five" + if oneVal == 6: oneWord = "Six" + if oneVal == 7: oneWord = "Seven" + if oneVal == 8: oneWord = "Eight" + if oneVal == 9: oneWord = "Nine" + if tenVal == 0: + numWord = ("%s %s" % (hunWord, oneWord)).strip() + else: + if oneVal == 0: + numWord = ("%s %s" % (hunWord, tenWord)).strip() + else: + numWord = ("%s %s-%s" % (hunWord, tenWord, oneWord)).strip() + + return numWord diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index d5f7e67b..c59bfed9 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- +# Qt Additions +from nw.gui.additions.qconfiglayout import QConfigLayout +from nw.gui.additions.qswitch import QSwitch + # Main Window Elements from nw.gui.icons import GuiIcons from nw.gui.mainmenu import GuiMainMenu @@ -29,9 +33,12 @@ from nw.gui.elements.viewdetails import GuiDocViewDetails # Tools from nw.gui.tools.dochighlight import GuiDocHighlighter +from nw.gui.tools.optionstate import OptionState from nw.gui.tools.wordcounter import WordCounter __all__ = [ + "QConfigLayout", + "QSwitch", "GuiIcons", "GuiMainMenu", "GuiMainStatus", @@ -54,5 +61,6 @@ __all__ = [ "GuiSearchBar", "GuiDocViewDetails", "GuiDocHighlighter", + "OptionState", "WordCounter", ] diff --git a/nw/gui/additions/__init__.py b/nw/gui/additions/__init__.py new file mode 100644 index 00000000..04f5e25a --- /dev/null +++ b/nw/gui/additions/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from nw.gui.additions.qconfiglayout import QConfigLayout +from nw.gui.additions.qswitch import QSwitch + +__all__ = [ + "QConfigLayout", + "QSwitch", +] diff --git a/nw/additions/qconfiglayout.py b/nw/gui/additions/qconfiglayout.py similarity index 100% rename from nw/additions/qconfiglayout.py rename to nw/gui/additions/qconfiglayout.py diff --git a/nw/additions/qswitch.py b/nw/gui/additions/qswitch.py similarity index 100% rename from nw/additions/qswitch.py rename to nw/gui/additions/qswitch.py diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/configeditor.py index ac0ed592..581d7307 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/configeditor.py @@ -38,8 +38,8 @@ from PyQt5.QtWidgets import ( QFileDialog ) -from nw.additions import QSwitch, QConfigLayout -from nw.tools import NWSpellCheck, NWSpellSimple, NWSpellEnchant +from nw.gui.additions import QSwitch, QConfigLayout +from nw.core import NWSpellCheck, NWSpellSimple, NWSpellEnchant from nw.constants import nwAlert, nwQuotes logger = logging.getLogger(__name__) diff --git a/nw/gui/dialogs/docmerge.py b/nw/gui/dialogs/docmerge.py index 887e8b72..097f4217 100644 --- a/nw/gui/dialogs/docmerge.py +++ b/nw/gui/dialogs/docmerge.py @@ -34,7 +34,7 @@ from PyQt5.QtWidgets import ( QListWidget, QAbstractItemView, QListWidgetItem ) from nw.constants import nwAlert, nwItemType -from nw.project import NWDoc +from nw.core import NWDoc logger = logging.getLogger(__name__) diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py index 55be4ee4..bfb4e8ee 100644 --- a/nw/gui/dialogs/docsplit.py +++ b/nw/gui/dialogs/docsplit.py @@ -34,7 +34,7 @@ from PyQt5.QtWidgets import ( QListWidget, QAbstractItemView, QListWidgetItem ) from nw.constants import nwAlert, nwItemType, nwItemClass, nwItemLayout -from nw.project import NWDoc +from nw.core import NWDoc logger = logging.getLogger(__name__) diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 5ed52888..a6d76958 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -30,7 +30,7 @@ import nw from time import time -from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtCore import Qt, QTimer, pyqtSlot from PyQt5.QtWidgets import ( qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QLabel ) @@ -39,10 +39,10 @@ from PyQt5.QtGui import ( QTextDocument, QCursor ) -from nw.project import NWDoc +from nw.core import NWDoc from nw.gui.tools import GuiDocHighlighter, WordCounter from nw.gui.elements.doctitlebar import GuiDocTitleBar -from nw.tools import NWSpellSimple +from nw.core import NWSpellSimple from nw.constants import nwUnicode, nwDocAction logger = logging.getLogger(__name__) @@ -589,6 +589,7 @@ class GuiDocEditor(QTextEdit): # Signals and Slots ## + @pyqtSlot(int, int, int) def _docChange(self, thePos, charsRemoved, charsAdded): """Triggered by QTextDocument->contentsChanged. This also triggers the syntax highlighter. @@ -602,6 +603,7 @@ class GuiDocEditor(QTextEdit): self._docAutoReplace(self.qDocument.findBlock(thePos)) return + @pyqtSlot("QPoint") def _openContextMenu(self, thePos): """Triggered by right click to open the context menu. Also triggered by the Ctrl+. shortcut. @@ -663,6 +665,7 @@ class GuiDocEditor(QTextEdit): self.hLight.rehighlightBlock(theCursor.block()) return + @pyqtSlot() def _runCounter(self): """Decide whether to run the word counter, or stop the timer due to inactivity. @@ -678,6 +681,7 @@ class GuiDocEditor(QTextEdit): self.wCounter.start() return + @pyqtSlot() def _updateCounts(self): """Slot for the word counter's finished signal """ @@ -730,6 +734,8 @@ class GuiDocEditor(QTextEdit): return True def _insertHardBreak(self): + """Inserts a hard line break at the cursor position. + """ theCursor = self.textCursor() theCursor.beginEditBlock() theCursor.insertText(" \n") @@ -737,6 +743,8 @@ class GuiDocEditor(QTextEdit): return def _insertNonBreakingSpace(self): + """Inserts a non-breaking space at the cursor position. + """ theCursor = self.textCursor() theCursor.beginEditBlock() theCursor.insertText(nwUnicode.U_NBSP) @@ -999,7 +1007,7 @@ class GuiDocEditor(QTextEdit): """ if self.mainConf.spellTool == "enchant": - from nw.tools.spellenchant import NWSpellEnchant + from nw.core.spellcheck import NWSpellEnchant self.theDict = NWSpellEnchant() else: self.theDict = NWSpellSimple() diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py index 09330346..92a006de 100644 --- a/nw/gui/elements/doctree.py +++ b/nw/gui/elements/doctree.py @@ -34,7 +34,7 @@ from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QAbstractItemView, QApplication, QMessageBox ) -from nw.project import NWDoc +from nw.core import NWDoc from nw.constants import ( nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert ) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index f2492910..d815dd54 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -34,7 +34,7 @@ from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QColor, QPixmap, QFont from PyQt5.QtWidgets import QStatusBar, QLabel -from nw.tools import NWSpellCheck +from nw.core import NWSpellCheck logger = logging.getLogger(__name__) diff --git a/nw/gui/tools/__init__.py b/nw/gui/tools/__init__.py index 4899f398..cc6cfdfa 100644 --- a/nw/gui/tools/__init__.py +++ b/nw/gui/tools/__init__.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- from nw.gui.tools.dochighlight import GuiDocHighlighter +from nw.gui.tools.optionstate import OptionState from nw.gui.tools.wordcounter import WordCounter __all__ = [ "GuiDocHighlighter", + "OptionState", "WordCounter", ] diff --git a/nw/tools/optionstate.py b/nw/gui/tools/optionstate.py similarity index 100% rename from nw/tools/optionstate.py rename to nw/gui/tools/optionstate.py diff --git a/nw/gui/tools/wordcounter.py b/nw/gui/tools/wordcounter.py index b7cb2045..24a0b9ca 100644 --- a/nw/gui/tools/wordcounter.py +++ b/nw/gui/tools/wordcounter.py @@ -30,7 +30,7 @@ import nw from PyQt5.QtCore import QThread -from nw.tools.wordcount import countWords +from nw.core.tools import countWords logger = logging.getLogger(__name__) diff --git a/nw/guimain.py b/nw/guimain.py index 935c1fb2..b3356301 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -45,8 +45,7 @@ from nw.gui import ( GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiProjectOutline, GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad ) -from nw.project import NWProject, NWDoc, NWIndex -from nw.tools import countWords +from nw.core import NWProject, NWDoc, NWIndex, countWords from nw.constants import nwFiles, nwItemType, nwAlert logger = logging.getLogger(__name__) diff --git a/nw/project/__init__.py b/nw/project/__init__.py deleted file mode 100644 index f2a0fe0c..00000000 --- a/nw/project/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- - -from nw.project.document import NWDoc -from nw.project.index import NWIndex -from nw.project.project import NWProject - -__all__ = [ - "NWDoc", - "NWIndex", - "NWProject", -] diff --git a/nw/tools/__init__.py b/nw/tools/__init__.py deleted file mode 100644 index d43736f9..00000000 --- a/nw/tools/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -from nw.tools.analyse import TextAnalysis -from nw.tools.legacy import projectMaintenance -from nw.tools.optionstate import OptionState -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 - -__all__ = [ - "TextAnalysis", - "projectMaintenance", - "OptionState", - "NWSpellCheck", - "NWSpellEnchant", - "NWSpellSimple", - "numberToWord", - "countWords", -] diff --git a/nw/tools/legacy.py b/nw/tools/legacy.py deleted file mode 100644 index b676caf7..00000000 --- a/nw/tools/legacy.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Legacy Tools - - novelWriter – Legacy Tools -============================ - Various functions to handle old projects - - File History: - Created: 2020-02-13 [0.4.3] - - This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import logging -import nw - -from os import path, unlink, rmdir - -logger = logging.getLogger(__name__) - -def projectMaintenance(theProject): - """Wrapper class for handling various tasks related to managing old - projects with content from older versions of novelWriter. - """ - - # Remove no longer used project cache folder - if path.isdir(theProject.projPath): - cacheDir = path.join(theProject.projPath, "cache") - if path.isdir(cacheDir): - logger.info("Deprecated cache folder found") - rmList = [] - for i in range(10): - rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i)) - rmList.append(path.join(cacheDir, "projCount.txt")) - for rmFile in rmList: - if path.isfile(rmFile): - logger.info("Deleting: %s" % rmFile) - try: - unlink(rmFile) - except Exception as e: - logger.error(str(e)) - logger.info("Deleting: %s" % cacheDir) - try: - rmdir(cacheDir) - except Exception as e: - logger.error(str(e)) - - # Remove no longer used meta files - rmList = [] - rmList.append(path.join(theProject.projMeta, "mainOptions.json")) - rmList.append(path.join(theProject.projMeta, "exportOptions.json")) - rmList.append(path.join(theProject.projMeta, "outlineOptions.json")) - rmList.append(path.join(theProject.projMeta, "timelineOptions.json")) - rmList.append(path.join(theProject.projMeta, "docMergeOptions.json")) - rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json")) - for rmFile in rmList: - if path.isfile(rmFile): - logger.info("Deleting: %s" % rmFile) - try: - unlink(rmFile) - except Exception as e: - logger.error(str(e)) - - return diff --git a/nw/tools/spellcheck.py b/nw/tools/spellcheck.py deleted file mode 100644 index a5b534cd..00000000 --- a/nw/tools/spellcheck.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- 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] - - This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import logging -import nw - -from os import path - -from nw.constants import isoLanguage - -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 - self.spellLanguage = None - return - - def setLanguage(self, theLang, projectDict=None): - return - - def checkWord(self, theWord): - return True - - def suggestWords(self, theWord): - 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="a+",encoding="utf-8") as outFile: - outFile.write("%s\n" % newWord) - except Exception as e: - logger.error("Failed to add word to project word list %s" % str(self.projectDict)) - logger.error(str(e)) - return - - def listDictionaries(self): - return [] - - @staticmethod - def expandLanguage(spTag): - spBits = spTag.split("_") - if spBits[0] in isoLanguage.ISO_639_1: - spLang = isoLanguage.ISO_639_1[spBits[0]] - else: - spLang = spBits[0] - if len(spBits) > 1: - spLang += " (%s)" % spBits[1] - return spLang - - ## - # Internal Functions - ## - - def _readProjectDictionary(self, projectDict): - self.PROJW = [] - if projectDict is not None: - self.projectDict = projectDict - if not path.isfile(projectDict): - return - 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 diff --git a/nw/tools/spellenchant.py b/nw/tools/spellenchant.py deleted file mode 100644 index 4e7400a0..00000000 --- a/nw/tools/spellenchant.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- 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] - - This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import logging -import nw -try: - import enchant -except: - # No need to do anything - # setLanguage will fall back to dummy dictionary - pass - -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): - """Load a dictionary for the language specified in the config. - If that fails, we load a dummy dictionary so that lookups don't - crash. - """ - try: - self.theDict = enchant.Dict(theLang) - self.spellLanguage = 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.spellLanguage = None - - self._readProjectDictionary(projectDict) - for pWord in self.PROJW: - self.theDict.add_to_session(pWord) - - 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_session(newWord) - NWSpellCheck.addWord(self, newWord) - return - - def listDictionaries(self): - retList = [] - for spTag, spProvider in enchant.list_dicts(): - spName = "%s [%s]" % (self.expandLanguage(spTag), spProvider.name) - retList.append((spTag, spName)) - return retList - -# END Class NWSpellEnchant - -class NWSpellEnchantDummy: - - def __init__(self): - return - - def check(self, theWord): - return True - - def suggest(self, theWord): - return [] - - def add_to_session(self, theWord): - return - -# END Class NWSpellEnchantDummy diff --git a/nw/tools/spellsimple.py b/nw/tools/spellsimple.py deleted file mode 100644 index 63a8847b..00000000 --- a/nw/tools/spellsimple.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- 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] - - This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -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 - -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)) - self.spellLanguage = theLang - except Exception as e: - logger.error("Failed to load spell check word list for language %s" % theLang) - logger.error(str(e)) - self.spellLanguage = None - - 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 - - spName = "%s [Internal]" % self.expandLanguage(theBits[0]) - retList.append((theBits[0], spName)) - - return retList - -# END Class NWSpellSimple diff --git a/nw/tools/translate.py b/nw/tools/translate.py deleted file mode 100644 index dd1f72f3..00000000 --- a/nw/tools/translate.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Translate Tools - - novelWriter – Translate Tools -=============================== - Various translate tools - - File History: - Created: 2019-10-13 [0.2.3] - - This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import logging -import nw - -logger = logging.getLogger(__name__) - -def numberToWord(numVal, theLanguage): - numWord = "" - if theLanguage == "en": - numWord = _numberToWordEN(numVal) - else: - numWord = _numberToWordEN(numVal) - # print("%4d : %s" % (numVal, numWord)) - return numWord - -def _numberToWordEN(numVal): - - numWord = "" - oneWord = "" - tenWord = "" - hunWord = "" - - if numVal == 0: - return "Zero" - - oneVal = numVal % 10 - tenVal = (numVal-oneVal) % 100 - hunVal = (numVal-tenVal-oneVal) % 1000 - - if hunVal == 100: hunWord = "One Hundred" - if hunVal == 200: hunWord = "Two Hundred" - if hunVal == 300: hunWord = "Three Hundred" - if hunVal == 400: hunWord = "Four Hundred" - if hunVal == 500: hunWord = "Five Hundred" - if hunVal == 600: hunWord = "Six Hundred" - if hunVal == 700: hunWord = "Seven Hundred" - if hunVal == 800: hunWord = "Eight Hundred" - if hunVal == 900: hunWord = "Nine Hundred" - - if tenVal == 20: tenWord = "Twenty" - if tenVal == 30: tenWord = "Thirty" - if tenVal == 40: tenWord = "Forty" - if tenVal == 50: tenWord = "Fifty" - if tenVal == 60: tenWord = "Sixty" - if tenVal == 70: tenWord = "Seventy" - if tenVal == 80: tenWord = "Eighty" - if tenVal == 90: tenWord = "Ninety" - - if tenVal == 10: - if oneVal == 0: oneWord = "Ten" - if oneVal == 1: oneWord = "Eleven" - if oneVal == 2: oneWord = "Twelve" - if oneVal == 3: oneWord = "Thirteen" - if oneVal == 4: oneWord = "Fourteen" - if oneVal == 5: oneWord = "Fifteen" - if oneVal == 6: oneWord = "Sixteen" - if oneVal == 7: oneWord = "Seventeen" - if oneVal == 8: oneWord = "Eighteen" - if oneVal == 9: oneWord = "Nineteen" - numWord = ("%s %s" % (hunWord, oneWord)).strip() - else: - if oneVal == 0: oneWord = "" - if oneVal == 1: oneWord = "One" - if oneVal == 2: oneWord = "Two" - if oneVal == 3: oneWord = "Three" - if oneVal == 4: oneWord = "Four" - if oneVal == 5: oneWord = "Five" - if oneVal == 6: oneWord = "Six" - if oneVal == 7: oneWord = "Seven" - if oneVal == 8: oneWord = "Eight" - if oneVal == 9: oneWord = "Nine" - if tenVal == 0: - numWord = ("%s %s" % (hunWord, oneWord)).strip() - else: - if oneVal == 0: - numWord = ("%s %s" % (hunWord, tenWord)).strip() - else: - numWord = ("%s %s-%s" % (hunWord, tenWord, oneWord)).strip() - - return numWord diff --git a/nw/tools/wordcount.py b/nw/tools/wordcount.py deleted file mode 100644 index 72043158..00000000 --- a/nw/tools/wordcount.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Word Counter - - novelWriter – Word Counter -============================ - Simple word counter - - File History: - Created: 2019-04-22 [0.0.1] - Moved: 2019-05-30 [0.1.4] - - This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import logging -import nw - -logger = logging.getLogger(__name__) - -def countWords(theText): - - charCount = 0 - wordCount = 0 - paraCount = 0 - prevEmpty = True - - for aLine in theText.splitlines(): - - countPara = True - theLen = len(aLine) - - if theLen == 0: - prevEmpty = True - continue - if aLine[0] == "@" or aLine[0] == "%": - continue - - if aLine[0:5] == "#### ": - wordCount -= 1 - charCount -= 5 - countPara = False - elif aLine[0:4] == "### ": - wordCount -= 1 - charCount -= 4 - countPara = False - elif aLine[0:3] == "## ": - wordCount -= 1 - charCount -= 3 - countPara = False - elif aLine[0:2] == "# ": - wordCount -= 1 - charCount -= 2 - countPara = False - - theBuff = aLine.replace("–"," ").replace("—"," ") - wordCount += len(theBuff.split()) - charCount += theLen - if countPara and prevEmpty: - paraCount += 1 - prevEmpty = countPara == False - - return charCount, wordCount, paraCount diff --git a/tests/test_item.py b/tests/test_item.py index b58f1b7e..64268e62 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -9,7 +9,7 @@ from lxml import etree from nwdummy import DummyMain from nw.config import Config -from nw.project.project import NWProject, NWItem +from nw.core.project import NWProject, NWItem from nw.constants import nwItemClass, nwItemType, nwItemLayout theConf = Config() diff --git a/tests/test_project.py b/tests/test_project.py index c029fbba..7d920a37 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -10,8 +10,8 @@ from nwtools import * from nwdummy import DummyMain from nw.config import Config -from nw.project.project import NWProject -from nw.project.index import NWIndex +from nw.core.project import NWProject +from nw.core.index import NWIndex from nw.constants import nwItemClass theConf = Config()