2019-02-22 09:32:34 +13:00
|
|
|
|
2019-02-22 12:50:28 +13:00
|
|
|
import os, gzip, json
|
2019-02-22 09:32:34 +13:00
|
|
|
from PyQt5.QtCore import QLocale
|
2019-02-22 12:50:28 +13:00
|
|
|
from collections import OrderedDict
|
|
|
|
from manuskript.functions import writablePath
|
2019-02-22 09:32:34 +13:00
|
|
|
|
|
|
|
try:
|
|
|
|
import enchant
|
|
|
|
except ImportError:
|
|
|
|
enchant = None
|
|
|
|
|
2019-02-22 12:50:28 +13:00
|
|
|
try:
|
|
|
|
import spellchecker as pyspellchecker
|
|
|
|
except ImportError:
|
|
|
|
pyspellchecker = None
|
|
|
|
|
|
|
|
|
2019-02-22 09:32:34 +13:00
|
|
|
class Spellchecker:
|
|
|
|
dictionaries = {}
|
2019-02-22 12:50:28 +13:00
|
|
|
# In order of priority
|
|
|
|
implementations = []
|
2019-02-22 09:32:34 +13:00
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def isInstalled():
|
2019-02-22 12:50:28 +13:00
|
|
|
for impl in Spellchecker.implementations:
|
|
|
|
if impl.isInstalled():
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def supportedLibraries():
|
|
|
|
libs = []
|
|
|
|
for impl in Spellchecker.implementations:
|
|
|
|
libs.append(impl.getLibraryName())
|
|
|
|
return libs
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def availableLibraries():
|
|
|
|
ret = []
|
|
|
|
for impl in Spellchecker.implementations:
|
|
|
|
if impl.isInstalled():
|
|
|
|
ret.append(impl.getLibraryName())
|
|
|
|
return ret
|
2019-02-22 09:32:34 +13:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def availableDictionaries():
|
2019-02-22 12:50:28 +13:00
|
|
|
dictionaries = OrderedDict()
|
|
|
|
for impl in Spellchecker.implementations:
|
|
|
|
dictionaries[impl.getLibraryName()] = impl.availableDictionaries()
|
|
|
|
return dictionaries
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def normalizeDictName(lib, dictionary):
|
|
|
|
return "{}:{}".format(lib, dictionary)
|
2019-02-22 09:32:34 +13:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getDefaultDictionary():
|
2019-02-22 12:50:28 +13:00
|
|
|
for impl in Spellchecker.implementations:
|
|
|
|
default = impl.getDefaultDictionary()
|
|
|
|
if default:
|
|
|
|
return Spellchecker.normalizeDictName(impl.getLibraryName(), default)
|
|
|
|
return None
|
2019-02-22 09:32:34 +13:00
|
|
|
|
|
|
|
@staticmethod
|
2019-02-22 12:50:28 +13:00
|
|
|
def getLibraryURL(lib=None):
|
|
|
|
urls = {}
|
|
|
|
for impl in Spellchecker.implementations:
|
|
|
|
urls[impl.getLibraryName()] = impl.getLibraryURL()
|
|
|
|
if lib:
|
|
|
|
return urls.get(lib, None)
|
|
|
|
return urls
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getResourcesPath(library):
|
|
|
|
path = os.path.join(writablePath(), "resources", "dictionaries", library)
|
|
|
|
if not os.path.exists(path):
|
|
|
|
os.makedirs(path)
|
|
|
|
return path
|
2019-02-22 09:32:34 +13:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getDictionary(dictionary):
|
|
|
|
if not dictionary:
|
|
|
|
dictionary = Spellchecker.getDefaultDictionary()
|
2019-02-22 12:50:28 +13:00
|
|
|
if not dictionary:
|
|
|
|
return None
|
|
|
|
|
|
|
|
values = dictionary.split(":", 1)
|
|
|
|
if len(values) == 1:
|
|
|
|
(lib, name) = (Spellchecker.implementations[0].getLibraryName(), dictionary)
|
|
|
|
dictionary = Spellchecker.normalizeDictName(lib, name)
|
|
|
|
else:
|
|
|
|
(lib, name) = values
|
2019-02-22 09:32:34 +13:00
|
|
|
try:
|
|
|
|
d = Spellchecker.dictionaries.get(dictionary, None)
|
|
|
|
if d is None:
|
2019-02-22 12:50:28 +13:00
|
|
|
for impl in Spellchecker.implementations:
|
|
|
|
if lib == impl.getLibraryName():
|
|
|
|
d = impl(name)
|
|
|
|
Spellchecker.dictionaries[dictionary] = d
|
|
|
|
break
|
2019-02-22 09:32:34 +13:00
|
|
|
return d
|
|
|
|
except Exception as e:
|
2019-02-22 12:50:28 +13:00
|
|
|
pass
|
|
|
|
return None
|
2019-02-22 09:32:34 +13:00
|
|
|
|
|
|
|
class BasicDictionary:
|
|
|
|
def __init__(self, name):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getLibraryName():
|
|
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getLibraryURL():
|
|
|
|
raise NotImplemented
|
|
|
|
|
2019-02-22 12:50:28 +13:00
|
|
|
@staticmethod
|
|
|
|
def isInstalled():
|
|
|
|
raise NotImplemented
|
|
|
|
|
2019-02-22 09:32:34 +13:00
|
|
|
@staticmethod
|
|
|
|
def getDefaultDictionary():
|
|
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def availableDictionaries():
|
|
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
def isMisspelled(self, word):
|
|
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
def getSuggestions(self, word):
|
|
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
def isCustomWord(self, word):
|
|
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
def addWord(self, word):
|
|
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
def removeWord(self, word):
|
|
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
class EnchantDictionary(BasicDictionary):
|
|
|
|
|
|
|
|
def __init__(self, name):
|
|
|
|
self._lang = name
|
|
|
|
if not (self._lang and enchant.dict_exists(self._lang)):
|
|
|
|
self._lang = self.getDefaultDictionary()
|
|
|
|
|
|
|
|
self._dict = enchant.Dict(self._lang)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
return self._lang
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getLibraryName():
|
2019-02-22 12:50:28 +13:00
|
|
|
return "PyEnchant"
|
2019-02-22 09:32:34 +13:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getLibraryURL():
|
|
|
|
return "https://pypi.org/project/pyenchant/"
|
|
|
|
|
2019-02-22 12:50:28 +13:00
|
|
|
@staticmethod
|
|
|
|
def isInstalled():
|
|
|
|
return enchant is not None
|
|
|
|
|
2019-02-22 09:32:34 +13:00
|
|
|
@staticmethod
|
|
|
|
def availableDictionaries():
|
2019-02-22 12:50:28 +13:00
|
|
|
if EnchantDictionary.isInstalled():
|
2019-02-22 09:32:34 +13:00
|
|
|
return list(map(lambda i: str(i[0]), enchant.list_dicts()))
|
|
|
|
return []
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getDefaultDictionary():
|
2019-02-22 12:50:28 +13:00
|
|
|
if not EnchantDictionary.isInstalled():
|
2019-02-22 09:32:34 +13:00
|
|
|
return None
|
|
|
|
|
|
|
|
default_locale = enchant.get_default_language()
|
|
|
|
if default_locale and not enchant.dict_exists(default_locale):
|
|
|
|
default_locale = None
|
|
|
|
|
|
|
|
if default_locale is None:
|
|
|
|
default_locale = QLocale.system().name()
|
|
|
|
if default_locale is None:
|
|
|
|
default_locale = self.availableDictionaries()[0]
|
|
|
|
|
|
|
|
return default_locale
|
|
|
|
|
|
|
|
def isMisspelled(self, word):
|
|
|
|
return not self._dict.check(word)
|
|
|
|
|
|
|
|
def getSuggestions(self, word):
|
|
|
|
return self._dict.suggest(word)
|
|
|
|
|
|
|
|
def isCustomWord(self, word):
|
|
|
|
return self._dict.is_added(word)
|
|
|
|
|
|
|
|
def addWord(self, word):
|
|
|
|
self._dict.add(word)
|
|
|
|
|
|
|
|
def removeWord(self, word):
|
|
|
|
self._dict.remove(word)
|
2019-02-22 12:50:28 +13:00
|
|
|
Spellchecker.implementations.append(EnchantDictionary)
|
|
|
|
|
|
|
|
|
|
|
|
class PySpellcheckerDictionary(BasicDictionary):
|
|
|
|
|
|
|
|
def __init__(self, name):
|
|
|
|
self._lang = name
|
|
|
|
if not self._lang:
|
|
|
|
self._lang = self.getDefaultDictionary()
|
|
|
|
|
|
|
|
self._dict = pyspellchecker.SpellChecker(self._lang)
|
|
|
|
self._customDict = None
|
|
|
|
customPath = self.getCustomDictionaryPath()
|
|
|
|
try:
|
|
|
|
self._customDict = pyspellchecker.SpellChecker(local_dictionary=customPath)
|
|
|
|
self._dict.word_frequency.load_dictionary(customPath)
|
|
|
|
except:
|
|
|
|
# If error loading the file, overwrite with empty dictionary
|
|
|
|
with gzip.open(customPath, "wt") as f:
|
|
|
|
f.write(json.dumps({}))
|
|
|
|
|
|
|
|
self._customDict = pyspellchecker.SpellChecker(local_dictionary=customPath)
|
|
|
|
self._dict.word_frequency.load_dictionary(customPath)
|
|
|
|
|
|
|
|
def getCustomDictionaryPath(self):
|
|
|
|
return os.path.join(Spellchecker.getResourcesPath(self.getLibraryName()), "{}.json.gz".format(self._lang))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
return self._lang
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getLibraryName():
|
|
|
|
return "pyspellchecker"
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getLibraryURL():
|
|
|
|
return "https://pyspellchecker.readthedocs.io/en/latest/"
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def isInstalled():
|
|
|
|
return pyspellchecker is not None
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def availableDictionaries():
|
|
|
|
if PySpellcheckerDictionary.isInstalled():
|
|
|
|
# TODO: If pyspellchecker eventually adds a way to get this list
|
|
|
|
# programmatically or if the list changes, we need to update it here
|
|
|
|
return ["de", "en", "es", "fr", "pt"]
|
|
|
|
return []
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getDefaultDictionary():
|
|
|
|
if not PySpellcheckerDictionary.isInstalled():
|
|
|
|
return None
|
|
|
|
|
|
|
|
default_locale = QLocale.system().name()
|
|
|
|
if default_locale:
|
|
|
|
default_locale = default_locale[0:2]
|
|
|
|
if default_locale is None:
|
|
|
|
default_locale = "en"
|
|
|
|
|
|
|
|
return default_locale
|
|
|
|
|
|
|
|
def isMisspelled(self, word):
|
|
|
|
return len(self._dict.unknown([word])) > 0
|
|
|
|
|
|
|
|
def getSuggestions(self, word):
|
|
|
|
candidates = self._dict.candidates(word)
|
|
|
|
if word in candidates:
|
|
|
|
candidates.remove(word)
|
|
|
|
return candidates
|
|
|
|
|
|
|
|
def isCustomWord(self, word):
|
|
|
|
return len(self._customDict.known([word])) > 0
|
|
|
|
|
|
|
|
def addWord(self, word):
|
|
|
|
self._dict.word_frequency.add(word)
|
|
|
|
self._customDict.word_frequency.add(word)
|
|
|
|
self._customDict.export(self.getCustomDictionaryPath(), gzipped=True)
|
|
|
|
|
|
|
|
def removeWord(self, word):
|
|
|
|
self._dict.word_frequency.remove(word)
|
|
|
|
self._customDict.word_frequency.remove(word)
|
|
|
|
self._customDict.export(self.getCustomDictionaryPath(), gzipped=True)
|
|
|
|
|
|
|
|
Spellchecker.implementations.append(PySpellcheckerDictionary)
|