diff --git a/novelwriter/common.py b/novelwriter/common.py index 7cb6c5e0..a4e37f62 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -40,7 +40,7 @@ from PyQt6.QtCore import QCoreApplication, QMimeData, QUrl from PyQt6.QtGui import QAction, QDesktopServices, QFont, QFontDatabase, QFontInfo from PyQt6.QtWidgets import QMenu, QMenuBar, QWidget -from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst +from novelwriter.constants import nwConst, nwLabels, nwQuotes, nwUnicode, trConst from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException @@ -298,6 +298,15 @@ def uniqueCompact(text: str) -> str: return "".join(sorted(set(compact(text)))) +def processDialogSymbols(symbols: str) -> str: + """Process dialogue line symbols.""" + result = "" + for c in uniqueCompact(symbols): + if c in nwQuotes.ALLOWED: + result += c + return result + + def elide(text: str, length: int) -> str: """Elide a piece of text to a maximum length.""" if len(text) > (cut := max(4, length)): diff --git a/novelwriter/config.py b/novelwriter/config.py index 0597c82e..d820c66a 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -42,7 +42,7 @@ from PyQt6.QtWidgets import QApplication from novelwriter.common import ( NWConfigParser, checkInt, checkPath, describeFont, fontMatcher, - formatTimeStamp + formatTimeStamp, processDialogSymbols ) from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode from novelwriter.error import formatException, logException @@ -680,7 +680,7 @@ class Config: self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath) self.dialogStyle = conf.rdInt(sec, "dialogstyle", self.dialogStyle) self.allowOpenDial = conf.rdBool(sec, "allowopendial", self.allowOpenDial) - self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine) + dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine) narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak) narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog) self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen) @@ -721,6 +721,7 @@ class Config: logger.info("Using straight double quotes, so disabling auto-replace") self.doReplaceDQuote = False + self.dialogLine = processDialogSymbols(dialogLine) self.narratorBreak = narratorBreak if narratorBreak in nwQuotes.DASHES else "" self.narratorDialog = narratorDialog if narratorDialog in nwQuotes.DASHES else "" diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 72ba4b90..16c5a082 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -512,6 +512,12 @@ class nwQuotes: "\u2015": QT_TRANSLATE_NOOP("Constant", "Horizontal bar"), } + ALLOWED: Final[list[str]] = [ + "\u0027", "\u0022", "\u2018", "\u2019", "\u201a", "\u201b", "\u201c", "\u201d", "\u201e", + "\u201f", "\u2e42", "\u2039", "\u203a", "\u00ab", "\u00bb", "\u300c", "\u300d", "\u300e", + "\u300f", "\u2013", "\u2014", "\u2015", + ] + class nwUnicode: """Supported unicode character constants and their HTML equivalents.""" diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 166b98be..f98374af 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -34,7 +34,7 @@ from PyQt6.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import compact, describeFont, uniqueCompact +from novelwriter.common import compact, describeFont, processDialogSymbols, uniqueCompact from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst from novelwriter.dialogs.quotes import GuiQuoteSelect @@ -1033,7 +1033,7 @@ class GuiPreferences(NDialog): # Text Highlighting dialogueStyle = self.dialogStyle.currentData() allowOpenDial = self.allowOpenDial.isChecked() - dialogueLine = uniqueCompact(self.dialogLine.text()) + dialogueLine = processDialogSymbols(self.dialogLine.text()) narratorBreak = self.narratorBreak.currentData() narratorDialog = self.narratorDialog.currentData() altDialogOpen = compact(self.altDialogOpen.text()) diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index bfd2b55c..006daa42 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -37,8 +37,9 @@ from novelwriter.common import ( formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe, - minmax, numberToRoman, openExternalPath, readTextFile, simplified, - transferCase, uniqueCompact, xmlElement, xmlIndent, xmlSubElem, yesNo + minmax, numberToRoman, openExternalPath, processDialogSymbols, + readTextFile, simplified, transferCase, uniqueCompact, xmlElement, + xmlIndent, xmlSubElem, yesNo ) from tests.mocked import causeOSError @@ -377,6 +378,14 @@ def testBaseCommon_uniqueCompact(): assert uniqueCompact("3 2 1") == "123" +@pytest.mark.base +def testBaseCommon_processDialogSymbols(): + """Test the processDialogSymbols function.""" + assert processDialogSymbols("abc") == "" + assert processDialogSymbols("\u00ab\u00ab\u00bb\u00bb") == "\u00ab\u00bb" + assert processDialogSymbols("-\u2013\u2014\u2015") == "\u2013\u2014\u2015" + + @pytest.mark.base def testBaseCommon_elide(): """Test the elide function."""