Restrict dialogue symbols (#2327)

This commit is contained in:
Veronica Berglyd Olsen
2025-05-04 22:22:15 +02:00
committed by GitHub
6 changed files with 60 additions and 26 deletions
+10 -1
View File
@@ -40,7 +40,7 @@ from PyQt6.QtCore import QCoreApplication, QMimeData, QUrl
from PyQt6.QtGui import QAction, QDesktopServices, QFont, QFontDatabase, QFontInfo from PyQt6.QtGui import QAction, QDesktopServices, QFont, QFontDatabase, QFontInfo
from PyQt6.QtWidgets import QMenu, QMenuBar, QWidget 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.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException from novelwriter.error import logException
@@ -298,6 +298,15 @@ def uniqueCompact(text: str) -> str:
return "".join(sorted(set(compact(text)))) 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: def elide(text: str, length: int) -> str:
"""Elide a piece of text to a maximum length.""" """Elide a piece of text to a maximum length."""
if len(text) > (cut := max(4, length)): if len(text) > (cut := max(4, length)):
+9 -5
View File
@@ -42,9 +42,9 @@ from PyQt6.QtWidgets import QApplication
from novelwriter.common import ( from novelwriter.common import (
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher, NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
formatTimeStamp formatTimeStamp, processDialogSymbols
) )
from novelwriter.constants import nwFiles, nwHtmlUnicode, nwUnicode from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode
from novelwriter.error import formatException, logException from novelwriter.error import formatException, logException
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -680,9 +680,9 @@ class Config:
self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath) self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath)
self.dialogStyle = conf.rdInt(sec, "dialogstyle", self.dialogStyle) self.dialogStyle = conf.rdInt(sec, "dialogstyle", self.dialogStyle)
self.allowOpenDial = conf.rdBool(sec, "allowopendial", self.allowOpenDial) self.allowOpenDial = conf.rdBool(sec, "allowopendial", self.allowOpenDial)
self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine) dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
self.narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak) narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak)
self.narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog) narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog)
self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen) self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen)
self.altDialogClose = conf.rdStr(sec, "altdialogclose", self.altDialogClose) self.altDialogClose = conf.rdStr(sec, "altdialogclose", self.altDialogClose)
self.highlightEmph = conf.rdBool(sec, "highlightemph", self.highlightEmph) self.highlightEmph = conf.rdBool(sec, "highlightemph", self.highlightEmph)
@@ -721,6 +721,10 @@ class Config:
logger.info("Using straight double quotes, so disabling auto-replace") logger.info("Using straight double quotes, so disabling auto-replace")
self.doReplaceDQuote = False 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 ""
return True return True
def saveConfig(self) -> bool: def saveConfig(self) -> bool:
+13
View File
@@ -505,6 +505,19 @@ class nwQuotes:
"\u300f": QT_TRANSLATE_NOOP("Constant", "Right white corner bracket"), "\u300f": QT_TRANSLATE_NOOP("Constant", "Right white corner bracket"),
} }
DASHES: Final[dict[str, str]] = {
"": QT_TRANSLATE_NOOP("Constant", "None"),
"\u2013": QT_TRANSLATE_NOOP("Constant", "Short dash"),
"\u2014": QT_TRANSLATE_NOOP("Constant", "Long dash"),
"\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: class nwUnicode:
"""Supported unicode character constants and their HTML equivalents.""" """Supported unicode character constants and their HTML equivalents."""
+15 -16
View File
@@ -34,9 +34,9 @@ from PyQt6.QtWidgets import (
) )
from novelwriter import CONFIG, SHARED 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.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL
from novelwriter.constants import nwLabels, nwUnicode, trConst from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst
from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm
from novelwriter.extensions.modified import ( from novelwriter.extensions.modified import (
@@ -636,21 +636,20 @@ class GuiPreferences(NDialog):
self.tr("Lines starting with any of these symbols are dialogue.") self.tr("Lines starting with any of these symbols are dialogue.")
) )
self.narratorBreak = QLineEdit(self) self.narratorBreak = NComboBox(self)
self.narratorBreak.setMaxLength(1) self.narratorDialog = NComboBox(self)
self.narratorBreak.setFixedWidth(boxFixed) for key, value in nwQuotes.DASHES.items():
self.narratorBreak.setAlignment(QtAlignCenter) label = trConst(value)
self.narratorBreak.setText(CONFIG.narratorBreak) self.narratorBreak.addItem(label, key)
self.narratorDialog.addItem(label, key)
self.narratorBreak.setCurrentData(CONFIG.narratorBreak, "")
self.narratorDialog.setCurrentData(CONFIG.narratorDialog, "")
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Narrator break symbol"), self.narratorBreak, self.tr("Narrator break symbol"), self.narratorBreak,
self.tr("Symbol to indicate a narrator break in dialogue.") self.tr("Symbol to indicate a narrator break in dialogue.")
) )
self.narratorDialog = QLineEdit(self)
self.narratorDialog.setMaxLength(1)
self.narratorDialog.setFixedWidth(boxFixed)
self.narratorDialog.setAlignment(QtAlignCenter)
self.narratorDialog.setText(CONFIG.narratorDialog)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Alternating dialogue/narration symbol"), self.narratorDialog, self.tr("Alternating dialogue/narration symbol"), self.narratorDialog,
self.tr("Alternates dialogue highlighting within any paragraph.") self.tr("Alternates dialogue highlighting within any paragraph.")
@@ -1034,9 +1033,9 @@ class GuiPreferences(NDialog):
# Text Highlighting # Text Highlighting
dialogueStyle = self.dialogStyle.currentData() dialogueStyle = self.dialogStyle.currentData()
allowOpenDial = self.allowOpenDial.isChecked() allowOpenDial = self.allowOpenDial.isChecked()
dialogueLine = uniqueCompact(self.dialogLine.text()) dialogueLine = processDialogSymbols(self.dialogLine.text())
narratorBreak = self.narratorBreak.text().strip() narratorBreak = self.narratorBreak.currentData()
narratorDialog = self.narratorDialog.text().strip() narratorDialog = self.narratorDialog.currentData()
altDialogOpen = compact(self.altDialogOpen.text()) altDialogOpen = compact(self.altDialogOpen.text())
altDialogClose = compact(self.altDialogClose.text()) altDialogClose = compact(self.altDialogClose.text())
highlightEmph = self.highlightEmph.isChecked() highlightEmph = self.highlightEmph.isChecked()
+11 -2
View File
@@ -37,8 +37,9 @@ from novelwriter.common import (
formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion, formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion,
fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout,
isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe, isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe,
minmax, numberToRoman, openExternalPath, readTextFile, simplified, minmax, numberToRoman, openExternalPath, processDialogSymbols,
transferCase, uniqueCompact, xmlElement, xmlIndent, xmlSubElem, yesNo readTextFile, simplified, transferCase, uniqueCompact, xmlElement,
xmlIndent, xmlSubElem, yesNo
) )
from tests.mocked import causeOSError from tests.mocked import causeOSError
@@ -377,6 +378,14 @@ def testBaseCommon_uniqueCompact():
assert uniqueCompact("3 2 1") == "123" 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 @pytest.mark.base
def testBaseCommon_elide(): def testBaseCommon_elide():
"""Test the elide function.""" """Test the elide function."""
+2 -2
View File
@@ -265,8 +265,8 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.dialogStyle.setCurrentData(3, 0) prefs.dialogStyle.setCurrentData(3, 0)
prefs.allowOpenDial.setChecked(False) prefs.allowOpenDial.setChecked(False)
prefs.dialogLine.setText("") prefs.dialogLine.setText("")
prefs.narratorBreak.setText("") prefs.narratorBreak.setCurrentData("", "")
prefs.narratorDialog.setText("") prefs.narratorDialog.setCurrentData("", "")
prefs.altDialogOpen.setText("<") prefs.altDialogOpen.setText("<")
prefs.altDialogClose.setText(">") prefs.altDialogClose.setText(">")
prefs.highlightEmph.setChecked(False) prefs.highlightEmph.setChecked(False)