Rewrite narrator break for dialogue highlighting (#2068)

This commit is contained in:
Veronica Berglyd Olsen
2024-10-27 13:15:57 +01:00
committed by GitHub
9 changed files with 114 additions and 100 deletions
+27 -27
View File
@@ -543,33 +543,6 @@ class GuiPreferences(NDialog):
self.tr("Applies to the selected quote styles.")
)
self.allowOpenDial = NSwitch(self)
self.allowOpenDial.setChecked(CONFIG.allowOpenDial)
self.mainForm.addRow(
self.tr("Allow open-ended dialogue"), self.allowOpenDial,
self.tr("Highlight dialogue line with no closing quote.")
)
self.narratorBreak = QLineEdit(self)
self.narratorBreak.setMaxLength(1)
self.narratorBreak.setFixedWidth(boxFixed)
self.narratorBreak.setAlignment(QtAlignCenter)
self.narratorBreak.setText(CONFIG.narratorBreak)
self.mainForm.addRow(
self.tr("Dialogue narrator break symbol"), self.narratorBreak,
self.tr("Symbol to indicate injected narrator break.")
)
self.dialogLine = QLineEdit(self)
self.dialogLine.setMaxLength(1)
self.dialogLine.setFixedWidth(boxFixed)
self.dialogLine.setAlignment(QtAlignCenter)
self.dialogLine.setText(CONFIG.dialogLine)
self.mainForm.addRow(
self.tr("Dialogue line symbol"), self.dialogLine,
self.tr("Lines starting with this symbol are dialogue.")
)
self.altDialogOpen = QLineEdit(self)
self.altDialogOpen.setMaxLength(4)
self.altDialogOpen.setFixedWidth(boxFixed)
@@ -587,6 +560,33 @@ class GuiPreferences(NDialog):
self.tr("Custom highlighting of dialogue text.")
)
self.allowOpenDial = NSwitch(self)
self.allowOpenDial.setChecked(CONFIG.allowOpenDial)
self.mainForm.addRow(
self.tr("Allow open-ended dialogue"), self.allowOpenDial,
self.tr("Highlight dialogue line with no closing quote.")
)
self.dialogLine = QLineEdit(self)
self.dialogLine.setMaxLength(1)
self.dialogLine.setFixedWidth(boxFixed)
self.dialogLine.setAlignment(QtAlignCenter)
self.dialogLine.setText(CONFIG.dialogLine)
self.mainForm.addRow(
self.tr("Dialogue line symbol"), self.dialogLine,
self.tr("Lines starting with this symbol are dialogue.")
)
self.narratorBreak = QLineEdit(self)
self.narratorBreak.setMaxLength(1)
self.narratorBreak.setFixedWidth(boxFixed)
self.narratorBreak.setAlignment(QtAlignCenter)
self.narratorBreak.setText(CONFIG.narratorBreak)
self.mainForm.addRow(
self.tr("Dialogue narrator break symbol"), self.narratorBreak,
self.tr("Symbol to indicate injected narrator break.")
)
self.highlightEmph = NSwitch(self)
self.highlightEmph.setChecked(CONFIG.highlightEmph)
self.mainForm.addRow(
+18 -10
View File
@@ -194,7 +194,10 @@ class Tokenizer(ABC):
nwShortcode.FOOTNOTE_B: TextFmt.FNOTE,
}
# Dialogue
self._rxDialogue: list[tuple[re.Pattern, tuple[int, str], tuple[int, str]]] = []
self._dialogLine = ""
self._narratorBreak = ""
return
@@ -331,21 +334,13 @@ class Tokenizer(ABC):
REGEX_PATTERNS.dialogStyle,
(TextFmt.COL_B, "dialog"), (TextFmt.COL_E, ""),
))
if CONFIG.dialogLine:
self._rxDialogue.append((
REGEX_PATTERNS.dialogLine,
(TextFmt.COL_B, "dialog"), (TextFmt.COL_E, ""),
))
if CONFIG.narratorBreak:
self._rxDialogue.append((
REGEX_PATTERNS.narratorBreak,
(TextFmt.COL_E, ""), (TextFmt.COL_B, "dialog"),
))
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
self._rxDialogue.append((
REGEX_PATTERNS.altDialogStyle,
(TextFmt.COL_B, "altdialog"), (TextFmt.COL_E, ""),
))
self._dialogLine = CONFIG.dialogLine.strip()[:1]
self._narratorBreak = CONFIG.narratorBreak.strip()[:1]
return
def setTitleMargins(self, upper: float, lower: float) -> None:
@@ -1149,6 +1144,19 @@ class Tokenizer(ABC):
temp.append((res.start(0), 0, fmtB, clsB))
temp.append((res.end(0), 0, fmtE, clsE))
if self._dialogLine and text.startswith(self._dialogLine):
if self._narratorBreak:
pos = 0
for num, bit in enumerate(text[1:].split(self._narratorBreak), 1):
length = len(bit) + 1
if num%2:
temp.append((pos, 0, TextFmt.COL_B, "dialog"))
temp.append((pos + length, 0, TextFmt.COL_E, ""))
pos += length
else:
temp.append((0, 0, TextFmt.COL_B, "dialog"))
temp.append((len(text), 0, TextFmt.COL_E, ""))
# Post-process text and format
result = text
formats = []
+19 -15
View File
@@ -59,7 +59,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
__slots__ = (
"_tHandle", "_isNovel", "_isInactive", "_spellCheck", "_spellErr",
"_hStyles", "_minRules", "_txtRules", "_cmnRules",
"_hStyles", "_minRules", "_txtRules", "_cmnRules", "_dialogLine",
"_narratorBreak",
)
def __init__(self, document: QTextDocument) -> None:
@@ -78,6 +79,9 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._txtRules: list[tuple[re.Pattern, dict[int, QTextCharFormat]]] = []
self._cmnRules: list[tuple[re.Pattern, dict[int, QTextCharFormat]]] = []
self._dialogLine = ""
self._narratorBreak = ""
self.initHighlighter()
logger.debug("Ready: GuiDocHighlighter")
@@ -132,6 +136,9 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._txtRules.clear()
self._cmnRules.clear()
self._dialogLine = CONFIG.dialogLine.strip()[:1]
self._narratorBreak = CONFIG.narratorBreak.strip()[:1]
# Multiple or Trailing Spaces
if CONFIG.showMultiSpaces:
rxRule = re.compile(r"[ ]{2,}|[ ]*$", re.UNICODE)
@@ -159,20 +166,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
}
self._txtRules.append((rxRule, hlRule))
if CONFIG.dialogLine:
rxRule = REGEX_PATTERNS.dialogLine
hlRule = {
0: self._hStyles["dialog"],
}
self._txtRules.append((rxRule, hlRule))
if CONFIG.narratorBreak:
rxRule = REGEX_PATTERNS.narratorBreak
hlRule = {
0: self._hStyles["text"],
}
self._txtRules.append((rxRule, hlRule))
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
rxRule = REGEX_PATTERNS.altDialogStyle
hlRule = {
@@ -411,6 +404,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setCurrentBlockState(BLOCK_TEXT)
hRules = self._txtRules if self._isNovel else self._minRules
if self._dialogLine and text.startswith(self._dialogLine):
if self._narratorBreak:
tPos = 0
for tNum, tBit in enumerate(text[1:].split(self._narratorBreak), 1):
tLen = len(tBit) + 1
if tNum%2:
self.setFormat(tPos, tLen, self._hStyles["dialog"])
tPos += tLen
else:
self.setFormat(0, len(text), self._hStyles["dialog"])
if hRules:
for rX, hRule in hRules:
for res in re.finditer(rX, text[xOff:]):
-12
View File
@@ -96,18 +96,6 @@ class RegExPatterns:
rxEnd = "|$" if CONFIG.allowOpenDial else ""
return re.compile(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})", re.UNICODE)
@property
def dialogLine(self) -> re.Pattern:
"""Dialogue line rule based on user settings."""
sym = re.escape(CONFIG.dialogLine)
return re.compile(f"^{sym}.*?$", re.UNICODE)
@property
def narratorBreak(self) -> re.Pattern:
"""Dialogue narrator break rule based on user settings."""
sym = re.escape(CONFIG.narratorBreak)
return re.compile(f"\\B{sym}\\S.*?\\S{sym}\\B", re.UNICODE)
@property
def altDialogStyle(self) -> re.Pattern:
"""Dialogue alternative rule based on user settings."""
@@ -1,8 +1,8 @@
%%~name: New Scene
%%~path: 000000000000d/000000000000f
%%~kind: NOVEL/DOCUMENT
%%~hash: 2a863dc53e09b0b1b0294ae7ea001853dddd596d
%%~date: 2023-10-17 20:52:56/2023-10-17 20:53:01
%%~hash: 0566024662fb6ed7ed645717addd64dabf058915
%%~date: 2024-10-27 13:03:13/2024-10-27 13:03:18
# Novel
## Chapter
@@ -34,6 +34,10 @@ This is another paragraph of much longer nonsense text. It is in fact 1 very ver
Some “ double quoted text with spaces padded ”.
Hi, I am a character speaking.
Hi, I am also a character speaking, said another character. How are you?
@object: NoSpaceAdded
% synopsis: No space before this colon.
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.4rc1" hexVersion="0x020400c1" fileVersion="1.5" fileRevision="3" timeStamp="2024-04-14 23:46:11">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="3">
<novelWriterXML appVersion="2.6a3" hexVersion="0x020600a3" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-27 13:01:24">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="6">
<name>New Project</name>
<author>Jane Doe</author>
</project>
@@ -28,7 +28,7 @@
<entry key="i000007" count="0" red="50" green="200" blue="0" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="11" novelWords="142" notesWords="27">
<content items="11" novelWords="161" notesWords="27">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="s000000" import="i000004">Novel</name>
@@ -46,7 +46,7 @@
<name status="s000000" import="i000004" active="yes">New Chapter</name>
</item>
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H1" charCount="808" wordCount="135" paraCount="15" cursorPos="1026" />
<meta expanded="no" heading="H1" charCount="918" wordCount="154" paraCount="17" cursorPos="1140" />
<name status="s000000" import="i000004" active="yes">New Scene</name>
</item>
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
+18 -3
View File
@@ -1251,8 +1251,6 @@ def testFmtToken_Dialogue(mockGUI):
CONFIG.dialogStyle = 3
CONFIG.altDialogOpen = "::"
CONFIG.altDialogClose = "::"
CONFIG.dialogLine = "\u2013"
CONFIG.narratorBreak = "\u2013"
project = NWProject()
tokens = BareTokenizer(project)
@@ -1305,7 +1303,24 @@ def testFmtToken_Dialogue(mockGUI):
BlockFmt.NONE
)]
# Dialogue line
CONFIG.dialogLine = "\u2013"
tokens.setDialogueHighlight(True)
tokens._text = "\u2013 Dialogue line without narrator break.\n"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.TEXT, "",
"\u2013 Dialogue line without narrator break.",
[
(0, TextFmt.COL_B, "dialog"),
(39, TextFmt.COL_E, ""),
],
BlockFmt.NONE
)]
# Dialogue line with narrator break
CONFIG.narratorBreak = "\u2013"
tokens.setDialogueHighlight(True)
tokens._text = "\u2013 Dialogue with a narrator break, \u2013he said,\u2013 see?\n"
tokens.tokenizeText()
assert tokens._blocks == [(
@@ -1314,7 +1329,7 @@ def testFmtToken_Dialogue(mockGUI):
[
(0, TextFmt.COL_B, "dialog"),
(34, TextFmt.COL_E, ""),
(44, TextFmt.COL_B, "dialog"),
(43, TextFmt.COL_B, "dialog"),
(49, TextFmt.COL_E, ""),
],
BlockFmt.NONE
+22 -1
View File
@@ -258,7 +258,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
# Syntax Highlighting
CONFIG.dialogStyle = 3
CONFIG.dialogLine = ""
CONFIG.narratorBreak = ""
CONFIG.altDialogOpen = "<|"
CONFIG.altDialogClose = "|>"
@@ -431,6 +430,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
# Dialogue
# ========
for c in "\"Full line double quoted text.\"":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
@@ -453,6 +455,25 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
docEditor._typPadBefore = ""
docEditor._typPadAfter = ""
# Dialogue Line
for c in "-- Hi, I am a character speaking.":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
# Narrator Break
CONFIG.narratorBreak = ""
docEditor = nwGUI.docEditor
docEditor._qDocument.syntaxHighlighter.initHighlighter()
for c in "-- Hi, I am also a character speaking, -- said another character. -- How are you?":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
# Special Formatting
# ==================
# Insert spaces before colon, but ignore tags and synopsis
docEditor._typPadBefore = ":"
-26
View File
@@ -327,35 +327,9 @@ def testTextPatterns_DialogueSpecial():
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
CONFIG.dialogStyle = 3
CONFIG.dialogLine = nwUnicode.U_ENDASH
CONFIG.narratorBreak = nwUnicode.U_ENDASH
CONFIG.altDialogOpen = "::"
CONFIG.altDialogClose = "::"
# Dialogue Line
# =============
regEx = REGEX_PATTERNS.dialogLine
# Check dialogue line in first position
assert allMatches(regEx, "\u2013 one two three") == [
[("\u2013 one two three", 0, 15)]
]
# Check dialogue line in second position
assert allMatches(regEx, " \u2013 one two three") == []
# Narrator Break
# ==============
regEx = REGEX_PATTERNS.narratorBreak
# Narrator break with no padding
assert allMatches(regEx, "one \u2013two\u2013 three") == [
[("\u2013two\u2013", 4, 9)]
]
# Narrator break with padding
assert allMatches(regEx, "one \u2013 two \u2013 three") == []
# Alternative Dialogue
# ====================
regEx = REGEX_PATTERNS.altDialogStyle