Rewrite narrator break for dialogue highlighting (#2068)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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:]):
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ":"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user