Update dialogue handling again (#2081)
This commit is contained in:
@@ -161,8 +161,9 @@ class Config:
|
||||
|
||||
self.dialogStyle = 2 # Quote type to use for dialogue
|
||||
self.allowOpenDial = True # Allow open-ended dialogue quotes
|
||||
self.narratorBreak = "" # Symbol to use for narrator break
|
||||
self.dialogLine = "" # Symbol to use for dialogue line
|
||||
self.narratorBreak = "" # Symbol to use for narrator break
|
||||
self.narratorDialog = "" # Symbol for alternating between dialogue and narrator
|
||||
self.altDialogOpen = "" # Alternative dialog symbol, open
|
||||
self.altDialogClose = "" # Alternative dialog symbol, close
|
||||
self.highlightEmph = True # Add colour to text emphasis
|
||||
@@ -657,10 +658,11 @@ 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)
|
||||
self.narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak)
|
||||
self.narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog)
|
||||
self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen)
|
||||
self.altDialogClose = conf.rdStr(sec, "altdialogclose", self.altDialogClose)
|
||||
self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
|
||||
self.highlightEmph = conf.rdBool(sec, "highlightemph", self.highlightEmph)
|
||||
self.stopWhenIdle = conf.rdBool(sec, "stopwhenidle", self.stopWhenIdle)
|
||||
self.userIdleTime = conf.rdInt(sec, "useridletime", self.userIdleTime)
|
||||
@@ -766,10 +768,11 @@ class Config:
|
||||
"showfullpath": str(self.showFullPath),
|
||||
"dialogstyle": str(self.dialogStyle),
|
||||
"allowopendial": str(self.allowOpenDial),
|
||||
"dialogline": str(self.dialogLine),
|
||||
"narratorbreak": str(self.narratorBreak),
|
||||
"narratordialog": str(self.narratorDialog),
|
||||
"altdialogopen": str(self.altDialogOpen),
|
||||
"altdialogclose": str(self.altDialogClose),
|
||||
"dialogline": str(self.dialogLine),
|
||||
"highlightemph": str(self.highlightEmph),
|
||||
"stopwhenidle": str(self.stopWhenIdle),
|
||||
"useridletime": str(self.userIdleTime),
|
||||
|
||||
@@ -583,8 +583,18 @@ class GuiPreferences(NDialog):
|
||||
self.narratorBreak.setAlignment(QtAlignCenter)
|
||||
self.narratorBreak.setText(CONFIG.narratorBreak)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Alternating dialogue/narration symbol"), self.narratorBreak,
|
||||
self.tr("Alternates dialogue highlighting within a paragraph.")
|
||||
self.tr("Dialogue narrator break symbol"), self.narratorBreak,
|
||||
self.tr("Symbol to indicate injected 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.tr("Alternating dialogue/narration symbol"), self.narratorDialog,
|
||||
self.tr("Alternates dialogue highlighting within any paragraph.")
|
||||
)
|
||||
|
||||
self.highlightEmph = NSwitch(self)
|
||||
@@ -952,8 +962,9 @@ class GuiPreferences(NDialog):
|
||||
# Text Highlighting
|
||||
dialogueStyle = self.dialogStyle.currentData()
|
||||
allowOpenDial = self.allowOpenDial.isChecked()
|
||||
narratorBreak = self.narratorBreak.text().strip()
|
||||
dialogueLine = uniqueCompact(self.dialogLine.text())
|
||||
narratorBreak = self.narratorBreak.text().strip()
|
||||
narratorDialog = self.narratorDialog.text().strip()
|
||||
altDialogOpen = compact(self.altDialogOpen.text())
|
||||
altDialogClose = compact(self.altDialogClose.text())
|
||||
highlightEmph = self.highlightEmph.isChecked()
|
||||
@@ -961,8 +972,9 @@ class GuiPreferences(NDialog):
|
||||
|
||||
updateSyntax |= CONFIG.dialogStyle != dialogueStyle
|
||||
updateSyntax |= CONFIG.allowOpenDial != allowOpenDial
|
||||
updateSyntax |= CONFIG.narratorBreak != narratorBreak
|
||||
updateSyntax |= CONFIG.dialogLine != dialogueLine
|
||||
updateSyntax |= CONFIG.narratorBreak != narratorBreak
|
||||
updateSyntax |= CONFIG.narratorDialog != narratorDialog
|
||||
updateSyntax |= CONFIG.altDialogOpen != altDialogOpen
|
||||
updateSyntax |= CONFIG.altDialogClose != altDialogClose
|
||||
updateSyntax |= CONFIG.highlightEmph != highlightEmph
|
||||
@@ -970,8 +982,9 @@ class GuiPreferences(NDialog):
|
||||
|
||||
CONFIG.dialogStyle = dialogueStyle
|
||||
CONFIG.allowOpenDial = allowOpenDial
|
||||
CONFIG.narratorBreak = narratorBreak
|
||||
CONFIG.dialogLine = dialogueLine
|
||||
CONFIG.narratorBreak = narratorBreak
|
||||
CONFIG.narratorDialog = narratorDialog
|
||||
CONFIG.altDialogOpen = altDialogOpen
|
||||
CONFIG.altDialogClose = altDialogClose
|
||||
CONFIG.highlightEmph = highlightEmph
|
||||
|
||||
@@ -86,26 +86,26 @@ class RegExPatterns:
|
||||
def dialogStyle(self) -> re.Pattern | None:
|
||||
"""Dialogue detection rule based on user settings."""
|
||||
if CONFIG.dialogStyle > 0:
|
||||
symO = ""
|
||||
symC = ""
|
||||
end = "|$" if CONFIG.allowOpenDial else ""
|
||||
rx = []
|
||||
if CONFIG.dialogStyle in (1, 3):
|
||||
symO += CONFIG.fmtSQuoteOpen.strip()[:1]
|
||||
symC += CONFIG.fmtSQuoteClose.strip()[:1]
|
||||
qO = CONFIG.fmtSQuoteOpen.strip()[:1]
|
||||
qC = CONFIG.fmtSQuoteClose.strip()[:1]
|
||||
rx.append(f"(?:\\B{qO}.*?(?:{qC}\\B{end}))")
|
||||
if CONFIG.dialogStyle in (2, 3):
|
||||
symO += CONFIG.fmtDQuoteOpen.strip()[:1]
|
||||
symC += CONFIG.fmtDQuoteClose.strip()[:1]
|
||||
|
||||
rxEnd = "|$" if CONFIG.allowOpenDial else ""
|
||||
return re.compile(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})", re.UNICODE)
|
||||
qO = CONFIG.fmtDQuoteOpen.strip()[:1]
|
||||
qC = CONFIG.fmtDQuoteClose.strip()[:1]
|
||||
rx.append(f"(?:\\B{qO}.*?(?:{qC}\\B{end}))")
|
||||
return re.compile("|".join(rx), re.UNICODE)
|
||||
return None
|
||||
|
||||
@property
|
||||
def altDialogStyle(self) -> re.Pattern | None:
|
||||
"""Dialogue alternative rule based on user settings."""
|
||||
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
||||
symO = re.escape(compact(CONFIG.altDialogOpen))
|
||||
symC = re.escape(compact(CONFIG.altDialogClose))
|
||||
return re.compile(f"\\B{symO}.*?{symC}\\B", re.UNICODE)
|
||||
qO = re.escape(compact(CONFIG.altDialogOpen))
|
||||
qC = re.escape(compact(CONFIG.altDialogClose))
|
||||
return re.compile(f"\\B{qO}.*?{qC}\\B", re.UNICODE)
|
||||
return None
|
||||
|
||||
|
||||
@@ -114,12 +114,13 @@ REGEX_PATTERNS = RegExPatterns()
|
||||
|
||||
class DialogParser:
|
||||
|
||||
__slots__ = ("_quotes", "_dialog", "_narrator", "_break", "_enabled")
|
||||
__slots__ = ("_quotes", "_dialog", "_narrator", "_alternate", "_break", "_enabled")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._quotes = None
|
||||
self._dialog = ""
|
||||
self._narrator = ""
|
||||
self._alternate = ""
|
||||
self._break = re.compile("")
|
||||
self._enabled = False
|
||||
return
|
||||
@@ -135,8 +136,9 @@ class DialogParser:
|
||||
self._quotes = REGEX_PATTERNS.dialogStyle
|
||||
self._dialog = uniqueCompact(CONFIG.dialogLine)
|
||||
self._narrator = CONFIG.narratorBreak.strip()[:1]
|
||||
self._alternate = CONFIG.narratorDialog.strip()[:1]
|
||||
self._break = re.compile(
|
||||
f"({self._narrator}\\s?.*?\\s?(?:{self._narrator}[{punct}]?|$))", re.UNICODE
|
||||
f"({self._narrator}\\s?.*?)(\\s?(?:{self._narrator}[{punct}]?|$))", re.UNICODE
|
||||
)
|
||||
self._enabled = bool(self._quotes or self._dialog or self._narrator)
|
||||
return
|
||||
@@ -147,26 +149,35 @@ class DialogParser:
|
||||
if text:
|
||||
plain = True
|
||||
if self._dialog and text[0] in self._dialog:
|
||||
# The whole line is dialogue
|
||||
plain = False
|
||||
temp.append(0)
|
||||
temp.append(len(text))
|
||||
if self._narrator:
|
||||
# Process narrator breaks in the dialogue
|
||||
for res in self._break.finditer(text, 1):
|
||||
temp.append(res.start(0))
|
||||
temp.append(res.end(0))
|
||||
if (two := res.group(2)) and two[0].isspace():
|
||||
temp.append(res.start(2))
|
||||
else:
|
||||
temp.append(res.end(0))
|
||||
elif self._quotes:
|
||||
# The line contains quoted dialogue
|
||||
for res in self._quotes.finditer(text):
|
||||
plain = False
|
||||
temp.append(res.start(0))
|
||||
temp.append(res.end(0))
|
||||
if self._narrator:
|
||||
for sub in self._break.finditer(text, res.start(0), res.end(0)):
|
||||
temp.append(sub.start(0))
|
||||
temp.append(sub.end(0))
|
||||
for res in self._break.finditer(text, 1):
|
||||
temp.append(res.start(0))
|
||||
if (two := res.group(2)) and two[0].isspace():
|
||||
temp.append(res.start(2))
|
||||
else:
|
||||
temp.append(res.end(0))
|
||||
|
||||
if plain and self._narrator:
|
||||
if plain and self._alternate:
|
||||
pos = 0
|
||||
for num, bit in enumerate(text.split(self._narrator)):
|
||||
for num, bit in enumerate(text.split(self._alternate)):
|
||||
length = len(bit) + int(num > 0)
|
||||
if num%2:
|
||||
temp.append(pos)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[Meta]
|
||||
timestamp = 2024-06-16 00:36:27
|
||||
timestamp = 2024-11-03 21:45:08
|
||||
|
||||
[Main]
|
||||
font =
|
||||
@@ -59,10 +59,11 @@ incnoteswcount = True
|
||||
showfullpath = True
|
||||
dialogstyle = 2
|
||||
allowopendial = True
|
||||
dialogline =
|
||||
narratorbreak =
|
||||
narratordialog =
|
||||
altdialogopen =
|
||||
altdialogclose =
|
||||
dialogline =
|
||||
highlightemph = True
|
||||
stopwhenidle = True
|
||||
useridletime = 300
|
||||
|
||||
@@ -247,8 +247,9 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, tstPaths):
|
||||
# Text Highlighting
|
||||
prefs.dialogStyle.setCurrentData(3, 0)
|
||||
prefs.allowOpenDial.setChecked(False)
|
||||
prefs.narratorBreak.setText("–")
|
||||
prefs.dialogLine.setText("–")
|
||||
prefs.narratorBreak.setText("–")
|
||||
prefs.narratorDialog.setText("–")
|
||||
prefs.altDialogOpen.setText("<")
|
||||
prefs.altDialogClose.setText(">")
|
||||
prefs.highlightEmph.setChecked(False)
|
||||
@@ -256,8 +257,9 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, tstPaths):
|
||||
|
||||
assert CONFIG.dialogStyle == 2
|
||||
assert CONFIG.allowOpenDial is True
|
||||
assert CONFIG.narratorBreak == ""
|
||||
assert CONFIG.dialogLine == ""
|
||||
assert CONFIG.narratorBreak == ""
|
||||
assert CONFIG.narratorDialog == ""
|
||||
assert CONFIG.altDialogOpen == ""
|
||||
assert CONFIG.altDialogClose == ""
|
||||
assert CONFIG.highlightEmph is True
|
||||
@@ -366,8 +368,9 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, tstPaths):
|
||||
# Text Highlighting
|
||||
assert CONFIG.dialogStyle == 3
|
||||
assert CONFIG.allowOpenDial is False
|
||||
assert CONFIG.narratorBreak == "–"
|
||||
assert CONFIG.dialogLine == "–"
|
||||
assert CONFIG.narratorBreak == "–"
|
||||
assert CONFIG.narratorDialog == "–"
|
||||
assert CONFIG.altDialogOpen == "<"
|
||||
assert CONFIG.altDialogClose == ">"
|
||||
assert CONFIG.highlightEmph is False
|
||||
|
||||
@@ -296,6 +296,16 @@ def testTextPatterns_DialogueStyle():
|
||||
[("\u201ctwo\u201d", 4, 9)]
|
||||
]
|
||||
|
||||
# Both single and double quotes are recognised
|
||||
assert allMatches(regEx, "one \u2018two\u2019 three \u201cfour\u201d five") == [
|
||||
[("\u2018two\u2019", 4, 9)], [("\u201cfour\u201d", 16, 22)]
|
||||
]
|
||||
|
||||
# But not mixed
|
||||
assert allMatches(regEx, "one \u2018two\u201d three \u201cfour\u2019 five") == [
|
||||
[("\u2018two\u201d three \u201cfour\u2019", 4, 22)]
|
||||
]
|
||||
|
||||
# Straight single quotes are ignored
|
||||
assert allMatches(regEx, "one 'two' three") == []
|
||||
|
||||
@@ -363,6 +373,7 @@ def testTextPatterns_DialogParserEnglish():
|
||||
|
||||
parser = DialogParser()
|
||||
parser.initParser()
|
||||
assert parser.enabled is True
|
||||
|
||||
# Positions: 0 18
|
||||
assert parser("“Simple dialogue.”") == [
|
||||
@@ -384,9 +395,14 @@ def testTextPatterns_DialogParserEnglish():
|
||||
CONFIG.narratorBreak = nwUnicode.U_EMDASH
|
||||
parser.initParser()
|
||||
|
||||
# Positions: 0 18 34 58
|
||||
# Positions: 0 18 32 58
|
||||
assert parser("“Simple dialogue, — argued John, — is not always so easy.”") == [
|
||||
(0, 18), (34, 58),
|
||||
(0, 18), (32, 58),
|
||||
]
|
||||
|
||||
# Positions: 0 18 32 56
|
||||
assert parser("“Simple dialogue, —argued John—, is not always so easy.”") == [
|
||||
(0, 18), (32, 56),
|
||||
]
|
||||
|
||||
|
||||
@@ -422,17 +438,15 @@ def testTextPatterns_DialogParserSpanish():
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_DialogParserAlternating():
|
||||
"""Test the dialog parser with alternating dialogue/narration like
|
||||
for Portuguese and Polish.
|
||||
"""
|
||||
def testTextPatterns_DialogParserPortuguese():
|
||||
"""Test the dialog parser with Portuguese settings."""
|
||||
# Set the config
|
||||
CONFIG.dialogStyle = 0
|
||||
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSAQUO
|
||||
CONFIG.fmtSQuoteClose = nwUnicode.U_RSAQUO
|
||||
CONFIG.fmtDQuoteOpen = nwUnicode.U_LAQUO
|
||||
CONFIG.fmtDQuoteClose = nwUnicode.U_RAQUO
|
||||
CONFIG.dialogLine = ""
|
||||
CONFIG.dialogLine = nwUnicode.U_EMDASH
|
||||
CONFIG.narratorBreak = nwUnicode.U_EMDASH
|
||||
|
||||
parser = DialogParser()
|
||||
@@ -448,7 +462,53 @@ def testTextPatterns_DialogParserAlternating():
|
||||
(0, 12),
|
||||
]
|
||||
|
||||
# Positions: 0 12 28 49
|
||||
# Positions: 0 12 27 49
|
||||
assert parser("— Tudo bem? — ele pergunta. — Você falou com ele?") == [
|
||||
(0, 12), (28, 49),
|
||||
(0, 12), (27, 49),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_DialogParserPolish():
|
||||
"""Test the dialog parser with alternating Polish settings."""
|
||||
# Set the config
|
||||
CONFIG.dialogStyle = 0
|
||||
CONFIG.fmtSQuoteOpen = "'"
|
||||
CONFIG.fmtSQuoteClose = "'"
|
||||
CONFIG.fmtDQuoteOpen = '"'
|
||||
CONFIG.fmtDQuoteClose = '"'
|
||||
CONFIG.dialogLine = ""
|
||||
CONFIG.narratorBreak = ""
|
||||
CONFIG.narratorDialog = nwUnicode.U_ENDASH
|
||||
|
||||
parser = DialogParser()
|
||||
parser.initParser()
|
||||
|
||||
# This is what an example dialogue might look like using Polish punctuation rules
|
||||
# See discussion #1976
|
||||
|
||||
assert parser(
|
||||
"– Example statement – someone said. And he added: – Another example statement."
|
||||
) == [
|
||||
(0, 20), (50, 78),
|
||||
]
|
||||
|
||||
assert parser(
|
||||
"– Oh my! – It would be nice if only the statements were highlighted, without "
|
||||
"any narration. In a paragraph where there is only a short statement and then "
|
||||
"a lot happens, this would be especially justified."
|
||||
) == [
|
||||
(0, 9),
|
||||
]
|
||||
|
||||
assert parser(
|
||||
"There are also sometimes paragraphs that start with a narrative, and only then "
|
||||
"someone shouts out the words: – Oooh! Look!"
|
||||
) == [
|
||||
(109, 122),
|
||||
]
|
||||
|
||||
assert parser(
|
||||
"And so on and so forth. However, \"text in quotation marks\" should not be "
|
||||
"highlighted at all, and if so, it should be highlighted differently."
|
||||
) == []
|
||||
|
||||
Reference in New Issue
Block a user