Merge patches from 1.6.3 (#1101)
This commit is contained in:
@@ -25,7 +25,7 @@ jobs:
|
|||||||
- name: Install Packages (apt)
|
- name: Install Packages (apt)
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install libenchant-dev qttools5-dev-tools
|
sudo apt install libenchant-dev qttools5-dev-tools aspell-en
|
||||||
- name: Checkout Source
|
- name: Checkout Source
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Install Dependencies (pip)
|
- name: Install Dependencies (pip)
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ class Config:
|
|||||||
# Check the availability of optional packages
|
# Check the availability of optional packages
|
||||||
self._checkOptionalPackages()
|
self._checkOptionalPackages()
|
||||||
|
|
||||||
if self.spellLanguage is None:
|
if not self.spellLanguage:
|
||||||
self.spellLanguage = "en"
|
self.spellLanguage = "en"
|
||||||
|
|
||||||
# Look for a PDF version of the manual
|
# Look for a PDF version of the manual
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -46,36 +48,47 @@ class NWSpellEnchant():
|
|||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Getters and Setters
|
# Properties
|
||||||
##
|
##
|
||||||
|
|
||||||
|
@property
|
||||||
def spellLanguage(self):
|
def spellLanguage(self):
|
||||||
return self._spellLanguage
|
return self._spellLanguage
|
||||||
|
|
||||||
|
##
|
||||||
|
# Setters
|
||||||
|
##
|
||||||
|
|
||||||
def setLanguage(self, theLang, projectDict=None):
|
def setLanguage(self, theLang, projectDict=None):
|
||||||
"""Load a dictionary for the language specified in the config.
|
"""Load a dictionary for the language specified in the config.
|
||||||
If that fails, we load a mock dictionary so that lookups don't
|
If that fails, we load a mock dictionary so that lookups don't
|
||||||
crash.
|
crash. Note that enchant will allow loading an empty string as
|
||||||
|
a tag, but this will fail later on. See issue #1096.
|
||||||
"""
|
"""
|
||||||
|
self._theBroker = None
|
||||||
|
self._theDict = None
|
||||||
|
self._spellLanguage = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import enchant
|
import enchant
|
||||||
if self._theBroker is not None:
|
|
||||||
logger.debug("Deleting old pyenchant broker")
|
|
||||||
del self._theBroker
|
|
||||||
|
|
||||||
self._theBroker = enchant.Broker()
|
if theLang and enchant.dict_exists(theLang):
|
||||||
self._theDict = self._theBroker.request_dict(theLang)
|
self._theBroker = enchant.Broker()
|
||||||
self._spellLanguage = theLang
|
self._theDict = self._theBroker.request_dict(theLang)
|
||||||
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
|
self._spellLanguage = theLang
|
||||||
|
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
|
||||||
|
else:
|
||||||
|
logger.warning("Enchant found no dictionary for language '%s'", theLang)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
|
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
|
||||||
self._theDict = FakeEnchant()
|
|
||||||
self._spellLanguage = None
|
|
||||||
|
|
||||||
self._readProjectDictionary(projectDict)
|
if self._theDict is None:
|
||||||
for pWord in self._projDict:
|
self._theDict = FakeEnchant()
|
||||||
self._theDict.add_to_session(pWord)
|
else:
|
||||||
|
self._readProjectDictionary(projectDict)
|
||||||
|
for pWord in self._projDict:
|
||||||
|
self._theDict.add_to_session(pWord)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -189,6 +202,9 @@ class FakeEnchant:
|
|||||||
"""Fallback for when Enchant is selected, but not installed.
|
"""Fallback for when Enchant is selected, but not installed.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.tag = ""
|
||||||
|
self.provider = namedtuple("provider", "name")
|
||||||
|
self.provider.name = ""
|
||||||
return
|
return
|
||||||
|
|
||||||
def check(self, theWord):
|
def check(self, theWord):
|
||||||
|
|||||||
@@ -712,7 +712,7 @@ class GuiDocEditor(QTextEdit):
|
|||||||
), nwAlert.INFO)
|
), nwAlert.INFO)
|
||||||
theMode = False
|
theMode = False
|
||||||
|
|
||||||
if self.spEnchant.spellLanguage() is None:
|
if self.spEnchant.spellLanguage is None:
|
||||||
theMode = False
|
theMode = False
|
||||||
|
|
||||||
self._spellCheck = theMode
|
self._spellCheck = theMode
|
||||||
@@ -1991,12 +1991,14 @@ class GuiDocEditor(QTextEdit):
|
|||||||
|
|
||||||
tCheck = tInsert
|
tCheck = tInsert
|
||||||
if tCheck in self.mainConf.fmtPadBefore:
|
if tCheck in self.mainConf.fmtPadBefore:
|
||||||
nDelete = max(nDelete, 1)
|
if self.allowSpaceBeforeColon(theText, tCheck):
|
||||||
tInsert = self._typPadChar + tInsert
|
nDelete = max(nDelete, 1)
|
||||||
|
tInsert = self._typPadChar + tInsert
|
||||||
|
|
||||||
if tCheck in self.mainConf.fmtPadAfter:
|
if tCheck in self.mainConf.fmtPadAfter:
|
||||||
nDelete = max(nDelete, 1)
|
if self.allowSpaceBeforeColon(theText, tCheck):
|
||||||
tInsert = tInsert + self._typPadChar
|
nDelete = max(nDelete, 1)
|
||||||
|
tInsert = tInsert + self._typPadChar
|
||||||
|
|
||||||
if nDelete > 0:
|
if nDelete > 0:
|
||||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
|
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
|
||||||
@@ -2004,6 +2006,20 @@ class GuiDocEditor(QTextEdit):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _allowSpaceBeforeColon(text, char):
|
||||||
|
"""Special checker function only used by the insert space
|
||||||
|
feature for French, Spanish, etc, so it doesn't insert a
|
||||||
|
space before colons in meta data lines. See issue #1090.
|
||||||
|
"""
|
||||||
|
if char == ":" and len(text) > 1:
|
||||||
|
if text[0] == "@":
|
||||||
|
return False
|
||||||
|
if text[0] == "%":
|
||||||
|
if text[1:].lstrip()[:9].lower() == "synopsis:":
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def _updateHeaders(self, checkPos=False, checkLevel=False):
|
def _updateHeaders(self, checkPos=False, checkLevel=False):
|
||||||
"""Update the headers record and return True if anything
|
"""Update the headers record and return True if anything
|
||||||
changed, if a check flag was provided.
|
changed, if a check flag was provided.
|
||||||
|
|||||||
@@ -26,29 +26,57 @@ import pytest
|
|||||||
from mock import causeOSError
|
from mock import causeOSError
|
||||||
from tools import readFile, writeFile
|
from tools import readFile, writeFile
|
||||||
|
|
||||||
from novelwriter.core.spellcheck import NWSpellEnchant
|
from novelwriter.core.spellcheck import FakeEnchant, NWSpellEnchant
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreSpell_Enchant(monkeypatch, tmpDir):
|
def testCoreSpell_FakeEnchant(monkeypatch):
|
||||||
"""Test the pyenchant spell checker
|
"""Test the FakeEnchant spell checker fallback.
|
||||||
"""
|
"""
|
||||||
wList = os.path.join(tmpDir, "wordlist.txt")
|
# Make package import fail
|
||||||
writeFile(wList, "a_word\nb_word\nc_word\n")
|
|
||||||
|
|
||||||
# Block the enchant package (and trigger the default class)
|
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setitem(sys.modules, "enchant", None)
|
mp.setitem(sys.modules, "enchant", None)
|
||||||
spChk = NWSpellEnchant()
|
spChk = NWSpellEnchant()
|
||||||
|
spChk.setLanguage("en_US", "")
|
||||||
|
assert isinstance(spChk._theDict, FakeEnchant)
|
||||||
|
|
||||||
spChk.setLanguage("en", wList)
|
# Request a non-existent dictionary
|
||||||
assert spChk.setLanguage("", "") is None
|
spChk = NWSpellEnchant()
|
||||||
assert spChk.checkWord("") is True
|
spChk.setLanguage("whatchamajig", "")
|
||||||
assert spChk.suggestWords("") == []
|
assert isinstance(spChk._theDict, FakeEnchant)
|
||||||
|
|
||||||
|
# Request an emety language string
|
||||||
|
# See issue https://github.com/vkbo/novelWriter/issues/1096
|
||||||
|
spChk = NWSpellEnchant()
|
||||||
|
spChk.setLanguage("", "")
|
||||||
|
assert isinstance(spChk._theDict, FakeEnchant)
|
||||||
|
|
||||||
|
# FakeEnchant should handle requests
|
||||||
|
fkChk = FakeEnchant()
|
||||||
|
assert fkChk.tag == ""
|
||||||
|
assert fkChk.provider.name == ""
|
||||||
|
assert fkChk.check("whatchamajig") is True
|
||||||
|
assert fkChk.suggest("whatchamajig") == []
|
||||||
|
assert fkChk.add_to_session("whatchamajig") is None
|
||||||
|
|
||||||
|
# END Test testCoreSpell_FakeEnchant
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreSpell_Enchant(monkeypatch, fncDir):
|
||||||
|
"""Test the pyenchant spell checker.
|
||||||
|
"""
|
||||||
|
wList = os.path.join(fncDir, "wordlist.txt")
|
||||||
|
writeFile(wList, "a_word\nb_word\nc_word\n")
|
||||||
|
|
||||||
|
# Break the enchant package, and check error handling
|
||||||
|
with monkeypatch.context() as mp:
|
||||||
|
mp.setitem(sys.modules, "enchant", None)
|
||||||
|
spChk = NWSpellEnchant()
|
||||||
assert spChk.listDictionaries() == []
|
assert spChk.listDictionaries() == []
|
||||||
assert spChk.describeDict() == ("", "")
|
assert spChk.describeDict() == ("", "")
|
||||||
|
|
||||||
# Break the enchant package, and check error handling
|
# Set the dict to None, and check dictionary call error handling
|
||||||
spChk = NWSpellEnchant()
|
spChk = NWSpellEnchant()
|
||||||
spChk.theDict = None
|
spChk.theDict = None
|
||||||
assert spChk.checkWord("word") is True
|
assert spChk.checkWord("word") is True
|
||||||
@@ -57,8 +85,9 @@ def testCoreSpell_Enchant(monkeypatch, tmpDir):
|
|||||||
|
|
||||||
# Load the proper enchant package (twice)
|
# Load the proper enchant package (twice)
|
||||||
spChk = NWSpellEnchant()
|
spChk = NWSpellEnchant()
|
||||||
spChk.setLanguage("en", wList)
|
spChk.setLanguage("en_US", wList)
|
||||||
spChk.setLanguage("en", wList)
|
spChk.setLanguage("en_US", wList)
|
||||||
|
assert spChk.spellLanguage == "en_US"
|
||||||
|
|
||||||
# Add a word to the user's dictionary
|
# Add a word to the user's dictionary
|
||||||
assert spChk._readProjectDictionary("stuff") is False
|
assert spChk._readProjectDictionary("stuff") is False
|
||||||
@@ -98,7 +127,39 @@ def testCoreSpell_Enchant(monkeypatch, tmpDir):
|
|||||||
assert len(dList) > 0
|
assert len(dList) > 0
|
||||||
|
|
||||||
aTag, aName = spChk.describeDict()
|
aTag, aName = spChk.describeDict()
|
||||||
assert aTag == "en"
|
assert aTag == "en_US"
|
||||||
assert aName != ""
|
assert aName != ""
|
||||||
|
|
||||||
# END Test testCoreSpell_Enchant
|
# END Test testCoreSpell_Enchant
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreSpell_SessionWords(fncDir):
|
||||||
|
"""Test the handling of the custom word list in the spell checker.
|
||||||
|
New project sessions should not inherit the project word list from
|
||||||
|
other sessions, so this test checks that they don't bleed through.
|
||||||
|
"""
|
||||||
|
wList1 = os.path.join(fncDir, "wordlist1.txt")
|
||||||
|
wList2 = os.path.join(fncDir, "wordlist2.txt")
|
||||||
|
writeFile(wList1, "a_word\nb_word\nc_word\n")
|
||||||
|
writeFile(wList2, "d_word\ne_word\nf_word\n")
|
||||||
|
|
||||||
|
spChk = NWSpellEnchant()
|
||||||
|
|
||||||
|
spChk.setLanguage("en_US", wList1)
|
||||||
|
assert spChk.checkWord("a_word") is True
|
||||||
|
assert spChk.checkWord("b_word") is True
|
||||||
|
assert spChk.checkWord("c_word") is True
|
||||||
|
assert spChk.checkWord("d_word") is False
|
||||||
|
assert spChk.checkWord("e_word") is False
|
||||||
|
assert spChk.checkWord("f_word") is False
|
||||||
|
|
||||||
|
spChk.setLanguage("en_US", wList2)
|
||||||
|
assert spChk.checkWord("a_word") is False
|
||||||
|
assert spChk.checkWord("b_word") is False
|
||||||
|
assert spChk.checkWord("c_word") is False
|
||||||
|
assert spChk.checkWord("d_word") is True
|
||||||
|
assert spChk.checkWord("e_word") is True
|
||||||
|
assert spChk.checkWord("f_word") is True
|
||||||
|
|
||||||
|
# END Test testCoreSpell_SessionWords
|
||||||
|
|||||||
@@ -1190,7 +1190,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testGuiEditor_WordCounters(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumText):
|
def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
|
||||||
"""Test saving text from the editor.
|
"""Test saving text from the editor.
|
||||||
"""
|
"""
|
||||||
# Block message box
|
# Block message box
|
||||||
@@ -1488,3 +1488,30 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum):
|
|||||||
# qtbot.stopForInteraction()
|
# qtbot.stopForInteraction()
|
||||||
|
|
||||||
# END Test testGuiEditor_Search
|
# END Test testGuiEditor_Search
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.gui
|
||||||
|
def testGuiEditor_StaticMethods():
|
||||||
|
"""Test the document editor's static methods.
|
||||||
|
"""
|
||||||
|
# Check the method that decides if it is allowed to insert a space
|
||||||
|
# before a colon using the French, Spanish, etc language feature
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("", "") is True
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("", ":") is True
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("some text", ":") is True
|
||||||
|
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("@:", ":") is False
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("@>", ">") is True
|
||||||
|
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("%", ":") is True
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("%:", ":") is True
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("%synopsis:", ":") is False
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis:", ":") is False
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("%synopsis :", ":") is True
|
||||||
|
assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis :", ":") is True
|
||||||
|
|
||||||
|
# END Test testGuiEditor_StaticMethods
|
||||||
|
|||||||
@@ -472,7 +472,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
|
|||||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx")
|
testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx")
|
||||||
compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx")
|
compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
assert cmpFiles(testFile, compFile, ignoreStart=(*XML_IGNORE, "<spellCheck"))
|
||||||
|
|
||||||
projFile = os.path.join(fncProj, "content", "000000000000f.nwd")
|
projFile = os.path.join(fncProj, "content", "000000000000f.nwd")
|
||||||
testFile = os.path.join(outDir, "guiEditor_Main_Final_000000000000f.nwd")
|
testFile = os.path.join(outDir, "guiEditor_Main_Final_000000000000f.nwd")
|
||||||
|
|||||||
Reference in New Issue
Block a user