Add a dialogue parser, and horizontal bar auto-complete (#2079)
This commit is contained in:
@@ -272,7 +272,7 @@ class NWBuildDocument:
|
||||
bldObj.setJustify(self._build.getBool("format.justifyText"))
|
||||
bldObj.setLineHeight(self._build.getFloat("format.lineHeight"))
|
||||
bldObj.setKeepLineBreaks(self._build.getBool("format.keepBreaks"))
|
||||
bldObj.setDialogueHighlight(self._build.getBool("format.showDialogue"))
|
||||
bldObj.setDialogHighlight(self._build.getBool("format.showDialogue"))
|
||||
bldObj.setFirstLineIndent(
|
||||
self._build.getBool("format.firstLineIndent"),
|
||||
self._build.getFloat("format.firstIndentWidth"),
|
||||
|
||||
@@ -34,7 +34,7 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import describeFont, uniqueCompact
|
||||
from novelwriter.common import compact, describeFont, uniqueCompact
|
||||
from novelwriter.constants import nwUnicode
|
||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||
from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm
|
||||
@@ -133,7 +133,7 @@ class GuiPreferences(NDialog):
|
||||
"""Build the settings form."""
|
||||
section = 0
|
||||
iSz = SHARED.theme.baseIconSize
|
||||
boxFixed = 5*SHARED.theme.textNWidth
|
||||
boxFixed = 6*SHARED.theme.textNWidth
|
||||
minWidth = CONFIG.pxInt(200)
|
||||
fontWidth = CONFIG.pxInt(162)
|
||||
|
||||
@@ -568,13 +568,13 @@ class GuiPreferences(NDialog):
|
||||
)
|
||||
|
||||
self.dialogLine = QLineEdit(self)
|
||||
self.dialogLine.setMaxLength(1)
|
||||
self.dialogLine.setMaxLength(4)
|
||||
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.tr("Dialogue line symbols"), self.dialogLine,
|
||||
self.tr("Lines starting with these symbols are always dialogue.")
|
||||
)
|
||||
|
||||
self.narratorBreak = QLineEdit(self)
|
||||
@@ -583,8 +583,8 @@ class GuiPreferences(NDialog):
|
||||
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.tr("Alternating dialogue/narration symbol"), self.narratorBreak,
|
||||
self.tr("Alternates dialogue highlighting within a paragraph.")
|
||||
)
|
||||
|
||||
self.highlightEmph = NSwitch(self)
|
||||
@@ -953,9 +953,9 @@ class GuiPreferences(NDialog):
|
||||
dialogueStyle = self.dialogStyle.currentData()
|
||||
allowOpenDial = self.allowOpenDial.isChecked()
|
||||
narratorBreak = self.narratorBreak.text().strip()
|
||||
dialogueLine = self.dialogLine.text().strip()
|
||||
altDialogOpen = self.altDialogOpen.text()
|
||||
altDialogClose = self.altDialogClose.text()
|
||||
dialogueLine = uniqueCompact(self.dialogLine.text())
|
||||
altDialogOpen = compact(self.altDialogOpen.text())
|
||||
altDialogClose = compact(self.altDialogClose.text())
|
||||
highlightEmph = self.highlightEmph.isChecked()
|
||||
showMultiSpaces = self.showMultiSpaces.isChecked()
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ from novelwriter.enum import nwComment, nwItemLayout
|
||||
from novelwriter.formats.shared import (
|
||||
BlockFmt, BlockTyp, T_Block, T_Formats, T_Note, TextDocumentTheme, TextFmt
|
||||
)
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -199,9 +199,10 @@ class Tokenizer(ABC):
|
||||
}
|
||||
|
||||
# Dialogue
|
||||
self._rxDialogue: list[tuple[re.Pattern, tuple[int, str], tuple[int, str]]] = []
|
||||
self._dialogLine = ""
|
||||
self._narratorBreak = ""
|
||||
self._hlightDialog = False
|
||||
self._rxAltDialog = REGEX_PATTERNS.altDialogStyle
|
||||
self._dialogParser = DialogParser()
|
||||
self._dialogParser.initParser()
|
||||
|
||||
return
|
||||
|
||||
@@ -335,22 +336,9 @@ class Tokenizer(ABC):
|
||||
self._doJustify = state
|
||||
return
|
||||
|
||||
def setDialogueHighlight(self, state: bool) -> None:
|
||||
def setDialogHighlight(self, state: bool) -> None:
|
||||
"""Enable or disable dialogue highlighting."""
|
||||
self._rxDialogue = []
|
||||
if state:
|
||||
if CONFIG.dialogStyle > 0:
|
||||
self._rxDialogue.append((
|
||||
REGEX_PATTERNS.dialogStyle,
|
||||
(TextFmt.COL_B, "dialog"), (TextFmt.COL_E, ""),
|
||||
))
|
||||
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]
|
||||
self._hlightDialog = state
|
||||
return
|
||||
|
||||
def setTitleMargins(self, upper: float, lower: float) -> None:
|
||||
@@ -1132,10 +1120,8 @@ class Tokenizer(ABC):
|
||||
|
||||
# Match URLs
|
||||
for res in REGEX_PATTERNS.url.finditer(text):
|
||||
s = res.start(0)
|
||||
e = res.end(0)
|
||||
temp.append((s, s, TextFmt.HRF_B, res.group(0)))
|
||||
temp.append((e, e, TextFmt.HRF_E, ""))
|
||||
temp.append((res.start(0), 0, TextFmt.HRF_B, res.group(0)))
|
||||
temp.append((res.end(0), 0, TextFmt.HRF_E, ""))
|
||||
|
||||
# Match Shortcodes
|
||||
for res in REGEX_PATTERNS.shortcodePlain.finditer(text):
|
||||
@@ -1156,24 +1142,15 @@ class Tokenizer(ABC):
|
||||
))
|
||||
|
||||
# Match Dialogue
|
||||
if self._rxDialogue and hDialog:
|
||||
for regEx, (fmtB, clsB), (fmtE, clsE) in self._rxDialogue:
|
||||
for res in regEx.finditer(text):
|
||||
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, ""))
|
||||
if self._hlightDialog and hDialog:
|
||||
if self._dialogParser.enabled:
|
||||
for pos, end in self._dialogParser(text):
|
||||
temp.append((pos, 0, TextFmt.COL_B, "dialog"))
|
||||
temp.append((end, 0, TextFmt.COL_E, ""))
|
||||
if self._rxAltDialog:
|
||||
for res in self._rxAltDialog.finditer(text):
|
||||
temp.append((res.start(0), 0, TextFmt.COL_B, "altdialog"))
|
||||
temp.append((res.end(0), 0, TextFmt.COL_E, ""))
|
||||
|
||||
# Post-process text and format
|
||||
result = text
|
||||
|
||||
+120
-93
@@ -36,6 +36,7 @@ import logging
|
||||
|
||||
from enum import Enum
|
||||
from time import time
|
||||
from typing import NamedTuple
|
||||
|
||||
from PyQt5.QtCore import (
|
||||
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
|
||||
@@ -83,6 +84,21 @@ class _SelectAction(Enum):
|
||||
MOVE_AFTER = 3
|
||||
|
||||
|
||||
class AutoReplaceConfig(NamedTuple):
|
||||
|
||||
typPadChar: str
|
||||
typSQuoteO: str
|
||||
typSQuoteC: str
|
||||
typDQuoteO: str
|
||||
typDQuoteC: str
|
||||
typRepDQuote: bool
|
||||
typRepSQuote: bool
|
||||
typRepDash: bool
|
||||
typRepDots: bool
|
||||
typPadBefore: str
|
||||
typPadAfter: str
|
||||
|
||||
|
||||
class GuiDocEditor(QPlainTextEdit):
|
||||
"""Gui Widget: Main Document Editor"""
|
||||
|
||||
@@ -128,17 +144,19 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self._doReplace = False # Switch to temporarily disable auto-replace
|
||||
|
||||
# Typography Cache
|
||||
self._typPadChar = " "
|
||||
self._typDQuoteO = '"'
|
||||
self._typDQuoteC = '"'
|
||||
self._typSQuoteO = "'"
|
||||
self._typSQuoteC = "'"
|
||||
self._typRepDQuote = False
|
||||
self._typRepSQuote = False
|
||||
self._typRepDash = False
|
||||
self._typRepDots = False
|
||||
self._typPadBefore = ""
|
||||
self._typPadAfter = ""
|
||||
self._typConf = AutoReplaceConfig(
|
||||
typPadChar=" ",
|
||||
typSQuoteO="'",
|
||||
typSQuoteC="'",
|
||||
typDQuoteO='"',
|
||||
typDQuoteC='"',
|
||||
typRepSQuote=False,
|
||||
typRepDQuote=False,
|
||||
typRepDash=False,
|
||||
typRepDots=False,
|
||||
typPadBefore="",
|
||||
typPadAfter="",
|
||||
)
|
||||
|
||||
# Completer
|
||||
self._completer = MetaCompleter(self)
|
||||
@@ -310,21 +328,19 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
created, and when the user changes the main editor preferences.
|
||||
"""
|
||||
# Typography
|
||||
if CONFIG.fmtPadThin:
|
||||
self._typPadChar = nwUnicode.U_THNBSP
|
||||
else:
|
||||
self._typPadChar = nwUnicode.U_NBSP
|
||||
|
||||
self._typSQuoteO = CONFIG.fmtSQuoteOpen
|
||||
self._typSQuoteC = CONFIG.fmtSQuoteClose
|
||||
self._typDQuoteO = CONFIG.fmtDQuoteOpen
|
||||
self._typDQuoteC = CONFIG.fmtDQuoteClose
|
||||
self._typRepDQuote = CONFIG.doReplaceDQuote
|
||||
self._typRepSQuote = CONFIG.doReplaceSQuote
|
||||
self._typRepDash = CONFIG.doReplaceDash
|
||||
self._typRepDots = CONFIG.doReplaceDots
|
||||
self._typPadBefore = CONFIG.fmtPadBefore
|
||||
self._typPadAfter = CONFIG.fmtPadAfter
|
||||
self._typConf = AutoReplaceConfig(
|
||||
typPadChar=nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP,
|
||||
typSQuoteO=CONFIG.fmtSQuoteOpen,
|
||||
typSQuoteC=CONFIG.fmtSQuoteClose,
|
||||
typDQuoteO=CONFIG.fmtDQuoteOpen,
|
||||
typDQuoteC=CONFIG.fmtDQuoteClose,
|
||||
typRepSQuote=CONFIG.doReplaceSQuote,
|
||||
typRepDQuote=CONFIG.doReplaceDQuote,
|
||||
typRepDash=CONFIG.doReplaceDash,
|
||||
typRepDots=CONFIG.doReplaceDots,
|
||||
typPadBefore=CONFIG.fmtPadBefore,
|
||||
typPadAfter=CONFIG.fmtPadAfter,
|
||||
)
|
||||
|
||||
# Reload spell check and dictionaries
|
||||
SHARED.updateSpellCheckLanguage()
|
||||
@@ -737,6 +753,7 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
logger.debug("Requesting action: %s", action.name)
|
||||
|
||||
tConf = self._typConf
|
||||
self._allowAutoReplace(False)
|
||||
if action == nwDocAction.UNDO:
|
||||
self.undo()
|
||||
@@ -755,9 +772,9 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
elif action == nwDocAction.MD_STRIKE:
|
||||
self._toggleFormat(2, "~")
|
||||
elif action == nwDocAction.S_QUOTE:
|
||||
self._wrapSelection(self._typSQuoteO, self._typSQuoteC)
|
||||
self._wrapSelection(tConf.typSQuoteO, tConf.typSQuoteC)
|
||||
elif action == nwDocAction.D_QUOTE:
|
||||
self._wrapSelection(self._typDQuoteO, self._typDQuoteC)
|
||||
self._wrapSelection(tConf.typDQuoteO, tConf.typDQuoteC)
|
||||
elif action == nwDocAction.SEL_ALL:
|
||||
self._makeSelection(QTextCursor.SelectionType.Document)
|
||||
elif action == nwDocAction.SEL_PARA:
|
||||
@@ -783,9 +800,9 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
elif action == nwDocAction.BLOCK_HSC:
|
||||
self._formatBlock(nwDocAction.BLOCK_HSC)
|
||||
elif action == nwDocAction.REPL_SNG:
|
||||
self._replaceQuotes("'", self._typSQuoteO, self._typSQuoteC)
|
||||
self._replaceQuotes("'", tConf.typSQuoteO, tConf.typSQuoteC)
|
||||
elif action == nwDocAction.REPL_DBL:
|
||||
self._replaceQuotes("\"", self._typDQuoteO, self._typDQuoteC)
|
||||
self._replaceQuotes("\"", tConf.typDQuoteO, tConf.typDQuoteC)
|
||||
elif action == nwDocAction.RM_BREAKS:
|
||||
self._removeInParLineBreaks()
|
||||
elif action == nwDocAction.ALIGN_L:
|
||||
@@ -857,13 +874,13 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
text = insert
|
||||
elif isinstance(insert, nwDocInsert):
|
||||
if insert == nwDocInsert.QUOTE_LS:
|
||||
text = self._typSQuoteO
|
||||
text = self._typConf.typSQuoteO
|
||||
elif insert == nwDocInsert.QUOTE_RS:
|
||||
text = self._typSQuoteC
|
||||
text = self._typConf.typSQuoteC
|
||||
elif insert == nwDocInsert.QUOTE_LD:
|
||||
text = self._typDQuoteO
|
||||
text = self._typConf.typDQuoteO
|
||||
elif insert == nwDocInsert.QUOTE_RD:
|
||||
text = self._typDQuoteC
|
||||
text = self._typConf.typDQuoteC
|
||||
elif insert == nwDocInsert.SYNOPSIS:
|
||||
text = "%Synopsis: "
|
||||
block = True
|
||||
@@ -1978,88 +1995,98 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
if tLen < 1 or tPos-1 > tLen:
|
||||
return
|
||||
|
||||
tOne = text[tPos-1:tPos]
|
||||
tTwo = text[tPos-2:tPos]
|
||||
tThree = text[tPos-3:tPos]
|
||||
t1 = text[tPos-1:tPos]
|
||||
t2 = text[tPos-2:tPos]
|
||||
t3 = text[tPos-3:tPos]
|
||||
t4 = text[tPos-4:tPos]
|
||||
|
||||
if not tOne:
|
||||
if not t1:
|
||||
return
|
||||
|
||||
nDelete = 0
|
||||
tInsert = tOne
|
||||
delete = 0
|
||||
insert = t1
|
||||
tConf = self._typConf
|
||||
|
||||
if self._typRepDQuote and tTwo[:1].isspace() and tTwo.endswith('"'):
|
||||
nDelete = 1
|
||||
tInsert = self._typDQuoteO
|
||||
if tConf.typRepDQuote and t2[:1].isspace() and t2.endswith('"'):
|
||||
delete = 1
|
||||
insert = tConf.typDQuoteO
|
||||
|
||||
elif self._typRepDQuote and tOne == '"':
|
||||
nDelete = 1
|
||||
elif tConf.typRepDQuote and t1 == '"':
|
||||
delete = 1
|
||||
if tPos == 1:
|
||||
tInsert = self._typDQuoteO
|
||||
elif tPos == 2 and tTwo == '>"':
|
||||
tInsert = self._typDQuoteO
|
||||
elif tPos == 3 and tThree == '>>"':
|
||||
tInsert = self._typDQuoteO
|
||||
insert = tConf.typDQuoteO
|
||||
elif tPos == 2 and t2 == '>"':
|
||||
insert = tConf.typDQuoteO
|
||||
elif tPos == 3 and t3 == '>>"':
|
||||
insert = tConf.typDQuoteO
|
||||
else:
|
||||
tInsert = self._typDQuoteC
|
||||
insert = tConf.typDQuoteC
|
||||
|
||||
elif self._typRepSQuote and tTwo[:1].isspace() and tTwo.endswith("'"):
|
||||
nDelete = 1
|
||||
tInsert = self._typSQuoteO
|
||||
elif tConf.typRepSQuote and t2[:1].isspace() and t2.endswith("'"):
|
||||
delete = 1
|
||||
insert = tConf.typSQuoteO
|
||||
|
||||
elif self._typRepSQuote and tOne == "'":
|
||||
nDelete = 1
|
||||
elif tConf.typRepSQuote and t1 == "'":
|
||||
delete = 1
|
||||
if tPos == 1:
|
||||
tInsert = self._typSQuoteO
|
||||
elif tPos == 2 and tTwo == ">'":
|
||||
tInsert = self._typSQuoteO
|
||||
elif tPos == 3 and tThree == ">>'":
|
||||
tInsert = self._typSQuoteO
|
||||
insert = tConf.typSQuoteO
|
||||
elif tPos == 2 and t2 == ">'":
|
||||
insert = tConf.typSQuoteO
|
||||
elif tPos == 3 and t3 == ">>'":
|
||||
insert = tConf.typSQuoteO
|
||||
else:
|
||||
tInsert = self._typSQuoteC
|
||||
insert = tConf.typSQuoteC
|
||||
|
||||
elif self._typRepDash and tThree == "---":
|
||||
nDelete = 3
|
||||
tInsert = nwUnicode.U_EMDASH
|
||||
elif tConf.typRepDash and t4 == "----":
|
||||
delete = 4
|
||||
insert = nwUnicode.U_HBAR
|
||||
|
||||
elif self._typRepDash and tTwo == "--":
|
||||
nDelete = 2
|
||||
tInsert = nwUnicode.U_ENDASH
|
||||
elif tConf.typRepDash and t3 == "---":
|
||||
delete = 3
|
||||
insert = nwUnicode.U_EMDASH
|
||||
|
||||
elif self._typRepDash and tTwo == nwUnicode.U_ENDASH + "-":
|
||||
nDelete = 2
|
||||
tInsert = nwUnicode.U_EMDASH
|
||||
elif tConf.typRepDash and t2 == "--":
|
||||
delete = 2
|
||||
insert = nwUnicode.U_ENDASH
|
||||
|
||||
elif self._typRepDots and tThree == "...":
|
||||
nDelete = 3
|
||||
tInsert = nwUnicode.U_HELLIP
|
||||
elif tConf.typRepDash and t2 == nwUnicode.U_ENDASH + "-":
|
||||
delete = 2
|
||||
insert = nwUnicode.U_EMDASH
|
||||
|
||||
elif tOne == nwUnicode.U_LSEP:
|
||||
elif tConf.typRepDash and t2 == nwUnicode.U_EMDASH + "-":
|
||||
delete = 2
|
||||
insert = nwUnicode.U_HBAR
|
||||
|
||||
elif tConf.typRepDots and t3 == "...":
|
||||
delete = 3
|
||||
insert = nwUnicode.U_HELLIP
|
||||
|
||||
elif t1 == nwUnicode.U_LSEP:
|
||||
# This resolves issue #1150
|
||||
nDelete = 1
|
||||
tInsert = nwUnicode.U_PSEP
|
||||
delete = 1
|
||||
insert = nwUnicode.U_PSEP
|
||||
|
||||
tCheck = tInsert
|
||||
if self._typPadBefore and tCheck in self._typPadBefore:
|
||||
if self._allowSpaceBeforeColon(text, tCheck):
|
||||
nDelete = max(nDelete, 1)
|
||||
chkPos = tPos - nDelete - 1
|
||||
check = insert
|
||||
if tConf.typPadBefore and check in tConf.typPadBefore:
|
||||
if self._allowSpaceBeforeColon(text, check):
|
||||
delete = max(delete, 1)
|
||||
chkPos = tPos - delete - 1
|
||||
if chkPos >= 0 and text[chkPos].isspace():
|
||||
# Strip existing space before inserting a new (#1061)
|
||||
nDelete += 1
|
||||
tInsert = self._typPadChar + tInsert
|
||||
delete += 1
|
||||
insert = tConf.typPadChar + insert
|
||||
|
||||
if self._typPadAfter and tCheck in self._typPadAfter:
|
||||
if self._allowSpaceBeforeColon(text, tCheck):
|
||||
nDelete = max(nDelete, 1)
|
||||
tInsert = tInsert + self._typPadChar
|
||||
if tConf.typPadAfter and check in tConf.typPadAfter:
|
||||
if self._allowSpaceBeforeColon(text, check):
|
||||
delete = max(delete, 1)
|
||||
insert = insert + tConf.typPadChar
|
||||
|
||||
if nDelete > 0:
|
||||
cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete)
|
||||
cursor.insertText(tInsert)
|
||||
if delete > 0:
|
||||
cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
|
||||
cursor.insertText(insert)
|
||||
|
||||
# Re-highlight, since the auto-replace sometimes interferes with it
|
||||
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
||||
# Re-highlight, since the auto-replace sometimes interferes with it
|
||||
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ from novelwriter.common import checkInt
|
||||
from novelwriter.constants import nwStyles, nwUnicode
|
||||
from novelwriter.core.index import processComment
|
||||
from novelwriter.enum import nwComment
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -59,8 +59,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
__slots__ = (
|
||||
"_tHandle", "_isNovel", "_isInactive", "_spellCheck", "_spellErr",
|
||||
"_hStyles", "_minRules", "_txtRules", "_cmnRules", "_dialogLine",
|
||||
"_narratorBreak",
|
||||
"_hStyles", "_minRules", "_txtRules", "_cmnRules", "_dialogParser",
|
||||
)
|
||||
|
||||
def __init__(self, document: QTextDocument) -> None:
|
||||
@@ -79,8 +78,7 @@ 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._dialogParser = DialogParser()
|
||||
|
||||
self.initHighlighter()
|
||||
|
||||
@@ -136,8 +134,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._txtRules.clear()
|
||||
self._cmnRules.clear()
|
||||
|
||||
self._dialogLine = CONFIG.dialogLine.strip()[:1]
|
||||
self._narratorBreak = CONFIG.narratorBreak.strip()[:1]
|
||||
self._dialogParser.initParser()
|
||||
|
||||
# Multiple or Trailing Spaces
|
||||
if CONFIG.showMultiSpaces:
|
||||
@@ -158,16 +155,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Dialogue
|
||||
if CONFIG.dialogStyle > 0:
|
||||
rxRule = REGEX_PATTERNS.dialogStyle
|
||||
hlRule = {
|
||||
0: self._hStyles["dialog"],
|
||||
}
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
|
||||
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
||||
rxRule = REGEX_PATTERNS.altDialogStyle
|
||||
# Alt Dialogue
|
||||
if rxRule := REGEX_PATTERNS.altDialogStyle:
|
||||
hlRule = {
|
||||
0: self._hStyles["altdialog"],
|
||||
}
|
||||
@@ -403,17 +392,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
else: # Text Paragraph
|
||||
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 self._dialogParser.enabled:
|
||||
for pos, end in self._dialogParser(text):
|
||||
length = end - pos
|
||||
self.setFormat(pos, length, self._hStyles["dialog"])
|
||||
|
||||
if hRules:
|
||||
for rX, hRule in hRules:
|
||||
|
||||
@@ -214,7 +214,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
sPos = self.verticalScrollBar().value()
|
||||
qDoc = ToQTextDocument(SHARED.project)
|
||||
qDoc.setJustify(CONFIG.doJustify)
|
||||
qDoc.setDialogueHighlight(True)
|
||||
qDoc.setDialogHighlight(True)
|
||||
qDoc.setFont(CONFIG.textFont)
|
||||
qDoc.setTheme(self._docTheme)
|
||||
qDoc.initDocument()
|
||||
|
||||
@@ -26,6 +26,7 @@ from __future__ import annotations
|
||||
import re
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.common import compact, uniqueCompact
|
||||
from novelwriter.constants import nwRegEx
|
||||
|
||||
|
||||
@@ -82,26 +83,103 @@ class RegExPatterns:
|
||||
return self._rxSCValue
|
||||
|
||||
@property
|
||||
def dialogStyle(self) -> re.Pattern:
|
||||
def dialogStyle(self) -> re.Pattern | None:
|
||||
"""Dialogue detection rule based on user settings."""
|
||||
symO = ""
|
||||
symC = ""
|
||||
if CONFIG.dialogStyle in (1, 3):
|
||||
symO += CONFIG.fmtSQuoteOpen
|
||||
symC += CONFIG.fmtSQuoteClose
|
||||
if CONFIG.dialogStyle in (2, 3):
|
||||
symO += CONFIG.fmtDQuoteOpen
|
||||
symC += CONFIG.fmtDQuoteClose
|
||||
if CONFIG.dialogStyle > 0:
|
||||
symO = ""
|
||||
symC = ""
|
||||
if CONFIG.dialogStyle in (1, 3):
|
||||
symO += CONFIG.fmtSQuoteOpen.strip()[:1]
|
||||
symC += CONFIG.fmtSQuoteClose.strip()[:1]
|
||||
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)
|
||||
rxEnd = "|$" if CONFIG.allowOpenDial else ""
|
||||
return re.compile(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})", re.UNICODE)
|
||||
return None
|
||||
|
||||
@property
|
||||
def altDialogStyle(self) -> re.Pattern:
|
||||
def altDialogStyle(self) -> re.Pattern | None:
|
||||
"""Dialogue alternative rule based on user settings."""
|
||||
symO = re.escape(CONFIG.altDialogOpen)
|
||||
symC = re.escape(CONFIG.altDialogClose)
|
||||
return re.compile(f"\\B{symO}.*?{symC}\\B", re.UNICODE)
|
||||
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)
|
||||
return None
|
||||
|
||||
|
||||
REGEX_PATTERNS = RegExPatterns()
|
||||
|
||||
|
||||
class DialogParser:
|
||||
|
||||
__slots__ = ("_quotes", "_dialog", "_narrator", "_break", "_enabled")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._quotes = None
|
||||
self._dialog = ""
|
||||
self._narrator = ""
|
||||
self._break = re.compile("")
|
||||
self._enabled = False
|
||||
return
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Return True if there are any settings to parse."""
|
||||
return self._enabled
|
||||
|
||||
def initParser(self) -> None:
|
||||
"""Init parser settings. Must be called when config changes."""
|
||||
punct = re.escape("!?.,:;")
|
||||
self._quotes = REGEX_PATTERNS.dialogStyle
|
||||
self._dialog = uniqueCompact(CONFIG.dialogLine)
|
||||
self._narrator = CONFIG.narratorBreak.strip()[:1]
|
||||
self._break = re.compile(
|
||||
f"({self._narrator}\\s?.*?\\s?(?:{self._narrator}[{punct}]?|$))", re.UNICODE
|
||||
)
|
||||
self._enabled = bool(self._quotes or self._dialog or self._narrator)
|
||||
return
|
||||
|
||||
def __call__(self, text: str) -> list[tuple[int, int]]:
|
||||
"""Caller wrapper for dialogue processing."""
|
||||
temp: list[int] = []
|
||||
if text:
|
||||
plain = True
|
||||
if self._dialog and text[0] in self._dialog:
|
||||
plain = False
|
||||
temp.append(0)
|
||||
temp.append(len(text))
|
||||
if self._narrator:
|
||||
for res in self._break.finditer(text, 1):
|
||||
temp.append(res.start(0))
|
||||
temp.append(res.end(0))
|
||||
elif self._quotes:
|
||||
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))
|
||||
|
||||
if plain and self._narrator:
|
||||
pos = 0
|
||||
for num, bit in enumerate(text.split(self._narrator)):
|
||||
length = len(bit) + int(num > 0)
|
||||
if num%2:
|
||||
temp.append(pos)
|
||||
temp.append(pos + length)
|
||||
pos += length
|
||||
|
||||
start = None
|
||||
result = []
|
||||
for pos in sorted(set(temp)):
|
||||
if start is None:
|
||||
start = pos
|
||||
else:
|
||||
result.append((start, pos))
|
||||
start = None
|
||||
|
||||
return result
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
%%~name: New Scene
|
||||
%%~path: 000000000000d/000000000000f
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 89b54ddaec2fddfd220e91dd438c84fb3aef9fc2
|
||||
%%~date: 2024-10-30 00:07:21/2024-10-30 00:07:26
|
||||
%%~hash: e4148ea77e78c90c334d5dc46c38a2b7904ac117
|
||||
%%~date: 2024-11-01 21:15:57/2024-11-01 21:16:01
|
||||
# Novel
|
||||
|
||||
## Chapter
|
||||
@@ -26,7 +26,7 @@ This is a paragraph of nonsense text.
|
||||
This is another paragraph
|
||||
with a line separator in it.
|
||||
|
||||
This is another paragraph of much longer nonsense text. It is in fact 1 very very NONSENSICAL nonsense text! We can also try replacing “quotes”, even single ‘quotes’ are replaced. Isn’t that nice? We can hyphen-ate, make dashes – and even longer dashes — if we want. Ellipsis? Not a problem either … How about three hyphens — for long dash? It works too.
|
||||
This is another paragraph of much longer nonsense text. It is in fact 1 very very NONSENSICAL nonsense text! We can also try replacing “quotes”, even single ‘quotes’ are replaced. Isn’t that nice? We can hyphen-ate, make dashes – and even longer dashes — if we want. We can even go on to a ― hotizontal bar. Ellipsis? Not a problem either … How about three hyphens — for long dash? It works too. Even four hyphens ― for a horizontal works!
|
||||
|
||||
“Full line double quoted text.”
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="2.6a3" hexVersion="0x020600a3" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-30 00:06:45">
|
||||
<novelWriterXML appVersion="2.6a3" hexVersion="0x020600a3" fileVersion="1.5" fileRevision="4" timeStamp="2024-11-01 21:15:11">
|
||||
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="5">
|
||||
<name>New Project</name>
|
||||
<author>Jane Doe</author>
|
||||
@@ -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="161" notesWords="27">
|
||||
<content items="11" novelWords="179" 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="918" wordCount="154" paraCount="17" cursorPos="1174" />
|
||||
<meta expanded="no" heading="H1" charCount="1003" wordCount="172" paraCount="17" cursorPos="1259" />
|
||||
<name status="s000000" import="i000004" active="yes">New Scene</name>
|
||||
</item>
|
||||
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
||||
|
||||
@@ -143,7 +143,6 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
|
||||
project = NWProject()
|
||||
html = ToHtml(project)
|
||||
html.initDocument()
|
||||
|
||||
html._isNovel = True
|
||||
html._isFirst = True
|
||||
|
||||
@@ -279,20 +278,31 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
|
||||
"<span style='color: #4271ae'><a href='#tag_europe'>Europe</a></span></p>\n"
|
||||
)
|
||||
|
||||
# Dialogue
|
||||
html.setDialogueHighlight(True)
|
||||
|
||||
@pytest.mark.core
|
||||
def testFmtToHtml_Dialog(mockGUI):
|
||||
"""Test paragraph formats in the ToHtml class."""
|
||||
CONFIG.altDialogOpen = "::"
|
||||
CONFIG.altDialogClose = "::"
|
||||
|
||||
project = NWProject()
|
||||
html = ToHtml(project)
|
||||
html.initDocument()
|
||||
html.setDialogHighlight(True)
|
||||
html._isNovel = True
|
||||
html._isFirst = True
|
||||
|
||||
# Dialog
|
||||
html.setDialogHighlight(True)
|
||||
html._text = "## Chapter\n\nThis text \u201chas dialogue\u201d in it.\n\n"
|
||||
html.tokenizeText()
|
||||
html.doConvert()
|
||||
assert html._pages[-1] == (
|
||||
"<h1 style='page-break-before: always;'>Chapter</h1>\n"
|
||||
"<h1>Chapter</h1>\n"
|
||||
"<p>This text <span style='color: #4271ae'>“has dialogue”</span> in it.</p>\n"
|
||||
)
|
||||
|
||||
# Alt. Dialogue
|
||||
CONFIG.altDialogOpen = "::"
|
||||
CONFIG.altDialogClose = "::"
|
||||
html.setDialogueHighlight(True)
|
||||
# Alt Dialog
|
||||
html._text = "## Chapter\n\nThis text ::has alt dialogue:: in it.\n\n"
|
||||
html.tokenizeText()
|
||||
html.doConvert()
|
||||
@@ -301,8 +311,15 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
|
||||
"<p>This text <span style='color: #813709'>::has alt dialogue::</span> in it.</p>\n"
|
||||
)
|
||||
|
||||
# Footnotes
|
||||
# =========
|
||||
|
||||
@pytest.mark.core
|
||||
def testFmtToHtml_Footnotes(mockGUI):
|
||||
"""Test paragraph formats in the ToHtml class."""
|
||||
project = NWProject()
|
||||
html = ToHtml(project)
|
||||
html.initDocument()
|
||||
html._isNovel = True
|
||||
html._isFirst = True
|
||||
|
||||
html._text = (
|
||||
"Text with one[footnote:fa] or two[footnote:fb] footnotes.\n\n"
|
||||
|
||||
@@ -1286,7 +1286,7 @@ def testFmtToken_Dialogue(mockGUI):
|
||||
|
||||
project = NWProject()
|
||||
tokens = BareTokenizer(project)
|
||||
tokens.setDialogueHighlight(True)
|
||||
tokens.setDialogHighlight(True)
|
||||
tokens._handle = TMH
|
||||
tokens._isNovel = True
|
||||
|
||||
@@ -1337,7 +1337,10 @@ def testFmtToken_Dialogue(mockGUI):
|
||||
|
||||
# Dialogue line
|
||||
CONFIG.dialogLine = "\u2013"
|
||||
tokens.setDialogueHighlight(True)
|
||||
tokens = BareTokenizer(project)
|
||||
tokens.setDialogHighlight(True)
|
||||
tokens._handle = TMH
|
||||
tokens._isNovel = True
|
||||
tokens._text = "\u2013 Dialogue line without narrator break.\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [(
|
||||
@@ -1352,16 +1355,19 @@ def testFmtToken_Dialogue(mockGUI):
|
||||
|
||||
# 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 = BareTokenizer(project)
|
||||
tokens.setDialogHighlight(True)
|
||||
tokens._handle = TMH
|
||||
tokens._isNovel = True
|
||||
tokens._text = "\u2013 Dialogue with a narrator break, \u2013he said\u2013, see?\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [(
|
||||
BlockTyp.TEXT, "",
|
||||
"\u2013 Dialogue with a narrator break, \u2013he said,\u2013 see?",
|
||||
"\u2013 Dialogue with a narrator break, \u2013he said\u2013, see?",
|
||||
[
|
||||
(0, TextFmt.COL_B, "dialog"),
|
||||
(34, TextFmt.COL_E, ""),
|
||||
(43, TextFmt.COL_B, "dialog"),
|
||||
(44, TextFmt.COL_B, "dialog"),
|
||||
(49, TextFmt.COL_E, ""),
|
||||
],
|
||||
BlockFmt.NONE
|
||||
|
||||
@@ -263,7 +263,7 @@ def testFmtToOdt_DialogueFormatting(mockGUI):
|
||||
"""Test formatting of dialogue."""
|
||||
project = NWProject()
|
||||
odt = ToOdt(project, isFlat=True)
|
||||
odt.setDialogueHighlight(True)
|
||||
odt.setDialogHighlight(True)
|
||||
odt.initDocument()
|
||||
oStyle = ODTParagraphStyle("test")
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ def testFmtToQTextDocument_TextCharFormats(mockGUI):
|
||||
|
||||
# Convert before init
|
||||
doc._text = "Blabla"
|
||||
doc.setDialogueHighlight(True)
|
||||
doc.setDialogHighlight(True)
|
||||
doc.doConvert()
|
||||
doc.tokenizeText()
|
||||
assert doc.document.toPlainText() == ""
|
||||
|
||||
@@ -80,7 +80,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
assert qDoc.defaultTextOption().alignment() == QtAlignLeft
|
||||
assert docEditor.verticalScrollBarPolicy() == QtScrollAsNeeded
|
||||
assert docEditor.horizontalScrollBarPolicy() == QtScrollAsNeeded
|
||||
assert docEditor._typPadChar == nwUnicode.U_NBSP
|
||||
assert docEditor._typConf.typPadChar == nwUnicode.U_NBSP
|
||||
assert docEditor.docHeader.itemTitle.text() == (
|
||||
"Novel \u203a New Chapter \u203a New Scene"
|
||||
)
|
||||
@@ -105,7 +105,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
assert qDoc.defaultTextOption().flags() & QTextOption.ShowLineAndParagraphSeparators
|
||||
assert docEditor.verticalScrollBarPolicy() == QtScrollAlwaysOff
|
||||
assert docEditor.horizontalScrollBarPolicy() == QtScrollAlwaysOff
|
||||
assert docEditor._typPadChar == nwUnicode.U_THNBSP
|
||||
assert docEditor._typConf.typPadChar == nwUnicode.U_THNBSP
|
||||
assert docEditor.docHeader.itemTitle.text() == "New Scene"
|
||||
|
||||
# Header
|
||||
|
||||
@@ -421,6 +421,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ":
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
for c in "We can even go on to a ---- hotizontal bar. ":
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
for c in "Ellipsis? Not a problem either ... ":
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
for c in "How about three hyphens - -":
|
||||
@@ -428,7 +430,17 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Left, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Backspace, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Right, delay=KEY_DELAY)
|
||||
for c in "- for long dash? It works too.":
|
||||
for c in "- for long dash? It works too. ":
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
for c in "Even four hyphens - - -":
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Left, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Backspace, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Left, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Backspace, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Right, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Right, delay=KEY_DELAY)
|
||||
for c in "- for a horizontal works!":
|
||||
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)
|
||||
@@ -447,16 +459,18 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
|
||||
|
||||
# Insert spaces before and after quotes
|
||||
docEditor._typPadBefore = "\u201d"
|
||||
docEditor._typPadAfter = "\u201c"
|
||||
CONFIG.fmtPadBefore = "\u201d"
|
||||
CONFIG.fmtPadAfter = "\u201c"
|
||||
docEditor.initEditor()
|
||||
|
||||
for c in "Some \"double quoted text with spaces padded\".":
|
||||
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)
|
||||
|
||||
docEditor._typPadBefore = ""
|
||||
docEditor._typPadAfter = ""
|
||||
CONFIG.fmtPadBefore = ""
|
||||
CONFIG.fmtPadAfter = ""
|
||||
docEditor.initEditor()
|
||||
|
||||
# Dialogue Line
|
||||
for c in "-- Hi, I am a character speaking.":
|
||||
@@ -478,7 +492,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
# ==================
|
||||
|
||||
# Insert spaces before colon, but ignore tags
|
||||
docEditor._typPadBefore = ":"
|
||||
CONFIG.fmtPadBefore = ":"
|
||||
docEditor.initEditor()
|
||||
|
||||
for c in "@object: NoSpaceAdded":
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
@@ -505,7 +520,8 @@ 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)
|
||||
|
||||
docEditor._typPadBefore = ""
|
||||
CONFIG.fmtPadBefore = ""
|
||||
docEditor.initEditor()
|
||||
|
||||
# Indent and Align
|
||||
# ================
|
||||
|
||||
@@ -26,7 +26,7 @@ import pytest
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwUnicode
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser
|
||||
|
||||
|
||||
def allMatches(regEx: re.Pattern, text: str) -> list[list[str]]:
|
||||
@@ -267,6 +267,10 @@ def testTextPatterns_ShortcodesValue():
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_DialogueStyle():
|
||||
"""Test the dialogue style pattern regexes."""
|
||||
# Before set, the regex is None
|
||||
CONFIG.dialogStyle = 0
|
||||
assert REGEX_PATTERNS.dialogStyle is None
|
||||
|
||||
# Set the config
|
||||
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
|
||||
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
|
||||
@@ -280,6 +284,7 @@ def testTextPatterns_DialogueStyle():
|
||||
|
||||
CONFIG.allowOpenDial = False
|
||||
regEx = REGEX_PATTERNS.dialogStyle
|
||||
assert regEx is not None
|
||||
|
||||
# Defined single quotes are recognised
|
||||
assert allMatches(regEx, "one \u2018two\u2019 three") == [
|
||||
@@ -305,6 +310,7 @@ def testTextPatterns_DialogueStyle():
|
||||
|
||||
CONFIG.allowOpenDial = True
|
||||
regEx = REGEX_PATTERNS.dialogStyle
|
||||
assert regEx is not None
|
||||
|
||||
# Defined single quotes are recognised also when open
|
||||
assert allMatches(regEx, "one \u2018two three") == [
|
||||
@@ -320,19 +326,19 @@ def testTextPatterns_DialogueStyle():
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_DialogueSpecial():
|
||||
"""Test the special dialogue style pattern regexes."""
|
||||
# Set the config
|
||||
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
|
||||
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
|
||||
CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO
|
||||
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
|
||||
# Before set, the regex is None
|
||||
CONFIG.altDialogOpen = ""
|
||||
CONFIG.altDialogClose = ""
|
||||
assert REGEX_PATTERNS.altDialogStyle is None
|
||||
|
||||
CONFIG.dialogStyle = 3
|
||||
# Set the config
|
||||
CONFIG.altDialogOpen = "::"
|
||||
CONFIG.altDialogClose = "::"
|
||||
|
||||
# Alternative Dialogue
|
||||
# ====================
|
||||
regEx = REGEX_PATTERNS.altDialogStyle
|
||||
assert regEx is not None
|
||||
|
||||
# With no padding
|
||||
assert allMatches(regEx, "one ::two:: three") == [
|
||||
@@ -343,3 +349,106 @@ def testTextPatterns_DialogueSpecial():
|
||||
assert allMatches(regEx, "one :: two :: three") == [
|
||||
[(":: two ::", 4, 13)]
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_DialogParserEnglish():
|
||||
"""Test the dialog parser with English settings."""
|
||||
# Set the config
|
||||
CONFIG.dialogStyle = 3
|
||||
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
|
||||
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
|
||||
CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO
|
||||
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
|
||||
|
||||
parser = DialogParser()
|
||||
parser.initParser()
|
||||
|
||||
# Positions: 0 18
|
||||
assert parser("“Simple dialogue.”") == [
|
||||
(0, 18),
|
||||
]
|
||||
|
||||
# Positions: 0 18
|
||||
assert parser("“Simple dialogue,” argued John.") == [
|
||||
(0, 18),
|
||||
]
|
||||
|
||||
# Positions: 0 18 32 56
|
||||
assert parser("“Simple dialogue,” argued John, “is not always so easy.”") == [
|
||||
(0, 18), (32, 56),
|
||||
]
|
||||
|
||||
# With Narrator breaks
|
||||
CONFIG.dialogLine = ""
|
||||
CONFIG.narratorBreak = nwUnicode.U_EMDASH
|
||||
parser.initParser()
|
||||
|
||||
# Positions: 0 18 34 58
|
||||
assert parser("“Simple dialogue, — argued John, — is not always so easy.”") == [
|
||||
(0, 18), (34, 58),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_DialogParserSpanish():
|
||||
"""Test the dialog parser with Spanish settings."""
|
||||
# Set the config
|
||||
CONFIG.dialogStyle = 3
|
||||
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSAQUO
|
||||
CONFIG.fmtSQuoteClose = nwUnicode.U_RSAQUO
|
||||
CONFIG.fmtDQuoteOpen = nwUnicode.U_LAQUO
|
||||
CONFIG.fmtDQuoteClose = nwUnicode.U_RAQUO
|
||||
CONFIG.dialogLine = nwUnicode.U_EMDASH + nwUnicode.U_RAQUO
|
||||
CONFIG.narratorBreak = nwUnicode.U_EMDASH
|
||||
|
||||
parser = DialogParser()
|
||||
parser.initParser()
|
||||
|
||||
# Positions: 0 18 54 70
|
||||
assert parser("—No te preocupes. —Cerró la puerta y salió corriendo—. Volveré pronto.") == [
|
||||
(0, 18), (54, 70),
|
||||
]
|
||||
|
||||
# Positions: 0 14
|
||||
assert parser("«Tengo hambre», pensó Pedro.") == [
|
||||
(0, 14),
|
||||
]
|
||||
|
||||
# Positions: 0 16
|
||||
assert parser("—Puedes hacerlo —le dije y pensé «pero te costará mucho trabajo».") == [
|
||||
(0, 16),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_DialogParserAlternating():
|
||||
"""Test the dialog parser with alternating dialogue/narration like
|
||||
for Portuguese and Polish.
|
||||
"""
|
||||
# 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.narratorBreak = nwUnicode.U_EMDASH
|
||||
|
||||
parser = DialogParser()
|
||||
parser.initParser()
|
||||
|
||||
# Positions: 0 21
|
||||
assert parser("— Está ficando tarde.") == [
|
||||
(0, 21),
|
||||
]
|
||||
|
||||
# Positions: 0 12
|
||||
assert parser("— Ainda não — ela responde.") == [
|
||||
(0, 12),
|
||||
]
|
||||
|
||||
# Positions: 0 12 28 49
|
||||
assert parser("— Tudo bem? — ele pergunta. — Você falou com ele?") == [
|
||||
(0, 12), (28, 49),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user