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.setJustify(self._build.getBool("format.justifyText"))
|
||||||
bldObj.setLineHeight(self._build.getFloat("format.lineHeight"))
|
bldObj.setLineHeight(self._build.getFloat("format.lineHeight"))
|
||||||
bldObj.setKeepLineBreaks(self._build.getBool("format.keepBreaks"))
|
bldObj.setKeepLineBreaks(self._build.getBool("format.keepBreaks"))
|
||||||
bldObj.setDialogueHighlight(self._build.getBool("format.showDialogue"))
|
bldObj.setDialogHighlight(self._build.getBool("format.showDialogue"))
|
||||||
bldObj.setFirstLineIndent(
|
bldObj.setFirstLineIndent(
|
||||||
self._build.getBool("format.firstLineIndent"),
|
self._build.getBool("format.firstLineIndent"),
|
||||||
self._build.getFloat("format.firstIndentWidth"),
|
self._build.getFloat("format.firstIndentWidth"),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from PyQt5.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
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.constants import nwUnicode
|
||||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||||
from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm
|
from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm
|
||||||
@@ -133,7 +133,7 @@ class GuiPreferences(NDialog):
|
|||||||
"""Build the settings form."""
|
"""Build the settings form."""
|
||||||
section = 0
|
section = 0
|
||||||
iSz = SHARED.theme.baseIconSize
|
iSz = SHARED.theme.baseIconSize
|
||||||
boxFixed = 5*SHARED.theme.textNWidth
|
boxFixed = 6*SHARED.theme.textNWidth
|
||||||
minWidth = CONFIG.pxInt(200)
|
minWidth = CONFIG.pxInt(200)
|
||||||
fontWidth = CONFIG.pxInt(162)
|
fontWidth = CONFIG.pxInt(162)
|
||||||
|
|
||||||
@@ -568,13 +568,13 @@ class GuiPreferences(NDialog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.dialogLine = QLineEdit(self)
|
self.dialogLine = QLineEdit(self)
|
||||||
self.dialogLine.setMaxLength(1)
|
self.dialogLine.setMaxLength(4)
|
||||||
self.dialogLine.setFixedWidth(boxFixed)
|
self.dialogLine.setFixedWidth(boxFixed)
|
||||||
self.dialogLine.setAlignment(QtAlignCenter)
|
self.dialogLine.setAlignment(QtAlignCenter)
|
||||||
self.dialogLine.setText(CONFIG.dialogLine)
|
self.dialogLine.setText(CONFIG.dialogLine)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Dialogue line symbol"), self.dialogLine,
|
self.tr("Dialogue line symbols"), self.dialogLine,
|
||||||
self.tr("Lines starting with this symbol are dialogue.")
|
self.tr("Lines starting with these symbols are always dialogue.")
|
||||||
)
|
)
|
||||||
|
|
||||||
self.narratorBreak = QLineEdit(self)
|
self.narratorBreak = QLineEdit(self)
|
||||||
@@ -583,8 +583,8 @@ class GuiPreferences(NDialog):
|
|||||||
self.narratorBreak.setAlignment(QtAlignCenter)
|
self.narratorBreak.setAlignment(QtAlignCenter)
|
||||||
self.narratorBreak.setText(CONFIG.narratorBreak)
|
self.narratorBreak.setText(CONFIG.narratorBreak)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Dialogue narrator break symbol"), self.narratorBreak,
|
self.tr("Alternating dialogue/narration symbol"), self.narratorBreak,
|
||||||
self.tr("Symbol to indicate injected narrator break.")
|
self.tr("Alternates dialogue highlighting within a paragraph.")
|
||||||
)
|
)
|
||||||
|
|
||||||
self.highlightEmph = NSwitch(self)
|
self.highlightEmph = NSwitch(self)
|
||||||
@@ -953,9 +953,9 @@ class GuiPreferences(NDialog):
|
|||||||
dialogueStyle = self.dialogStyle.currentData()
|
dialogueStyle = self.dialogStyle.currentData()
|
||||||
allowOpenDial = self.allowOpenDial.isChecked()
|
allowOpenDial = self.allowOpenDial.isChecked()
|
||||||
narratorBreak = self.narratorBreak.text().strip()
|
narratorBreak = self.narratorBreak.text().strip()
|
||||||
dialogueLine = self.dialogLine.text().strip()
|
dialogueLine = uniqueCompact(self.dialogLine.text())
|
||||||
altDialogOpen = self.altDialogOpen.text()
|
altDialogOpen = compact(self.altDialogOpen.text())
|
||||||
altDialogClose = self.altDialogClose.text()
|
altDialogClose = compact(self.altDialogClose.text())
|
||||||
highlightEmph = self.highlightEmph.isChecked()
|
highlightEmph = self.highlightEmph.isChecked()
|
||||||
showMultiSpaces = self.showMultiSpaces.isChecked()
|
showMultiSpaces = self.showMultiSpaces.isChecked()
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ from novelwriter.enum import nwComment, nwItemLayout
|
|||||||
from novelwriter.formats.shared import (
|
from novelwriter.formats.shared import (
|
||||||
BlockFmt, BlockTyp, T_Block, T_Formats, T_Note, TextDocumentTheme, TextFmt
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -199,9 +199,10 @@ class Tokenizer(ABC):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Dialogue
|
# Dialogue
|
||||||
self._rxDialogue: list[tuple[re.Pattern, tuple[int, str], tuple[int, str]]] = []
|
self._hlightDialog = False
|
||||||
self._dialogLine = ""
|
self._rxAltDialog = REGEX_PATTERNS.altDialogStyle
|
||||||
self._narratorBreak = ""
|
self._dialogParser = DialogParser()
|
||||||
|
self._dialogParser.initParser()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -335,22 +336,9 @@ class Tokenizer(ABC):
|
|||||||
self._doJustify = state
|
self._doJustify = state
|
||||||
return
|
return
|
||||||
|
|
||||||
def setDialogueHighlight(self, state: bool) -> None:
|
def setDialogHighlight(self, state: bool) -> None:
|
||||||
"""Enable or disable dialogue highlighting."""
|
"""Enable or disable dialogue highlighting."""
|
||||||
self._rxDialogue = []
|
self._hlightDialog = state
|
||||||
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]
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def setTitleMargins(self, upper: float, lower: float) -> None:
|
def setTitleMargins(self, upper: float, lower: float) -> None:
|
||||||
@@ -1132,10 +1120,8 @@ class Tokenizer(ABC):
|
|||||||
|
|
||||||
# Match URLs
|
# Match URLs
|
||||||
for res in REGEX_PATTERNS.url.finditer(text):
|
for res in REGEX_PATTERNS.url.finditer(text):
|
||||||
s = res.start(0)
|
temp.append((res.start(0), 0, TextFmt.HRF_B, res.group(0)))
|
||||||
e = res.end(0)
|
temp.append((res.end(0), 0, TextFmt.HRF_E, ""))
|
||||||
temp.append((s, s, TextFmt.HRF_B, res.group(0)))
|
|
||||||
temp.append((e, e, TextFmt.HRF_E, ""))
|
|
||||||
|
|
||||||
# Match Shortcodes
|
# Match Shortcodes
|
||||||
for res in REGEX_PATTERNS.shortcodePlain.finditer(text):
|
for res in REGEX_PATTERNS.shortcodePlain.finditer(text):
|
||||||
@@ -1156,24 +1142,15 @@ class Tokenizer(ABC):
|
|||||||
))
|
))
|
||||||
|
|
||||||
# Match Dialogue
|
# Match Dialogue
|
||||||
if self._rxDialogue and hDialog:
|
if self._hlightDialog and hDialog:
|
||||||
for regEx, (fmtB, clsB), (fmtE, clsE) in self._rxDialogue:
|
if self._dialogParser.enabled:
|
||||||
for res in regEx.finditer(text):
|
for pos, end in self._dialogParser(text):
|
||||||
temp.append((res.start(0), 0, fmtB, clsB))
|
temp.append((pos, 0, TextFmt.COL_B, "dialog"))
|
||||||
temp.append((res.end(0), 0, fmtE, clsE))
|
temp.append((end, 0, TextFmt.COL_E, ""))
|
||||||
|
if self._rxAltDialog:
|
||||||
if self._dialogLine and text.startswith(self._dialogLine):
|
for res in self._rxAltDialog.finditer(text):
|
||||||
if self._narratorBreak:
|
temp.append((res.start(0), 0, TextFmt.COL_B, "altdialog"))
|
||||||
pos = 0
|
temp.append((res.end(0), 0, TextFmt.COL_E, ""))
|
||||||
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
|
# Post-process text and format
|
||||||
result = text
|
result = text
|
||||||
|
|||||||
+120
-93
@@ -36,6 +36,7 @@ import logging
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from time import time
|
from time import time
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
|
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
|
||||||
@@ -83,6 +84,21 @@ class _SelectAction(Enum):
|
|||||||
MOVE_AFTER = 3
|
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):
|
class GuiDocEditor(QPlainTextEdit):
|
||||||
"""Gui Widget: Main Document Editor"""
|
"""Gui Widget: Main Document Editor"""
|
||||||
|
|
||||||
@@ -128,17 +144,19 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self._doReplace = False # Switch to temporarily disable auto-replace
|
self._doReplace = False # Switch to temporarily disable auto-replace
|
||||||
|
|
||||||
# Typography Cache
|
# Typography Cache
|
||||||
self._typPadChar = " "
|
self._typConf = AutoReplaceConfig(
|
||||||
self._typDQuoteO = '"'
|
typPadChar=" ",
|
||||||
self._typDQuoteC = '"'
|
typSQuoteO="'",
|
||||||
self._typSQuoteO = "'"
|
typSQuoteC="'",
|
||||||
self._typSQuoteC = "'"
|
typDQuoteO='"',
|
||||||
self._typRepDQuote = False
|
typDQuoteC='"',
|
||||||
self._typRepSQuote = False
|
typRepSQuote=False,
|
||||||
self._typRepDash = False
|
typRepDQuote=False,
|
||||||
self._typRepDots = False
|
typRepDash=False,
|
||||||
self._typPadBefore = ""
|
typRepDots=False,
|
||||||
self._typPadAfter = ""
|
typPadBefore="",
|
||||||
|
typPadAfter="",
|
||||||
|
)
|
||||||
|
|
||||||
# Completer
|
# Completer
|
||||||
self._completer = MetaCompleter(self)
|
self._completer = MetaCompleter(self)
|
||||||
@@ -310,21 +328,19 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
created, and when the user changes the main editor preferences.
|
created, and when the user changes the main editor preferences.
|
||||||
"""
|
"""
|
||||||
# Typography
|
# Typography
|
||||||
if CONFIG.fmtPadThin:
|
self._typConf = AutoReplaceConfig(
|
||||||
self._typPadChar = nwUnicode.U_THNBSP
|
typPadChar=nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP,
|
||||||
else:
|
typSQuoteO=CONFIG.fmtSQuoteOpen,
|
||||||
self._typPadChar = nwUnicode.U_NBSP
|
typSQuoteC=CONFIG.fmtSQuoteClose,
|
||||||
|
typDQuoteO=CONFIG.fmtDQuoteOpen,
|
||||||
self._typSQuoteO = CONFIG.fmtSQuoteOpen
|
typDQuoteC=CONFIG.fmtDQuoteClose,
|
||||||
self._typSQuoteC = CONFIG.fmtSQuoteClose
|
typRepSQuote=CONFIG.doReplaceSQuote,
|
||||||
self._typDQuoteO = CONFIG.fmtDQuoteOpen
|
typRepDQuote=CONFIG.doReplaceDQuote,
|
||||||
self._typDQuoteC = CONFIG.fmtDQuoteClose
|
typRepDash=CONFIG.doReplaceDash,
|
||||||
self._typRepDQuote = CONFIG.doReplaceDQuote
|
typRepDots=CONFIG.doReplaceDots,
|
||||||
self._typRepSQuote = CONFIG.doReplaceSQuote
|
typPadBefore=CONFIG.fmtPadBefore,
|
||||||
self._typRepDash = CONFIG.doReplaceDash
|
typPadAfter=CONFIG.fmtPadAfter,
|
||||||
self._typRepDots = CONFIG.doReplaceDots
|
)
|
||||||
self._typPadBefore = CONFIG.fmtPadBefore
|
|
||||||
self._typPadAfter = CONFIG.fmtPadAfter
|
|
||||||
|
|
||||||
# Reload spell check and dictionaries
|
# Reload spell check and dictionaries
|
||||||
SHARED.updateSpellCheckLanguage()
|
SHARED.updateSpellCheckLanguage()
|
||||||
@@ -737,6 +753,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
logger.debug("Requesting action: %s", action.name)
|
logger.debug("Requesting action: %s", action.name)
|
||||||
|
|
||||||
|
tConf = self._typConf
|
||||||
self._allowAutoReplace(False)
|
self._allowAutoReplace(False)
|
||||||
if action == nwDocAction.UNDO:
|
if action == nwDocAction.UNDO:
|
||||||
self.undo()
|
self.undo()
|
||||||
@@ -755,9 +772,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
elif action == nwDocAction.MD_STRIKE:
|
elif action == nwDocAction.MD_STRIKE:
|
||||||
self._toggleFormat(2, "~")
|
self._toggleFormat(2, "~")
|
||||||
elif action == nwDocAction.S_QUOTE:
|
elif action == nwDocAction.S_QUOTE:
|
||||||
self._wrapSelection(self._typSQuoteO, self._typSQuoteC)
|
self._wrapSelection(tConf.typSQuoteO, tConf.typSQuoteC)
|
||||||
elif action == nwDocAction.D_QUOTE:
|
elif action == nwDocAction.D_QUOTE:
|
||||||
self._wrapSelection(self._typDQuoteO, self._typDQuoteC)
|
self._wrapSelection(tConf.typDQuoteO, tConf.typDQuoteC)
|
||||||
elif action == nwDocAction.SEL_ALL:
|
elif action == nwDocAction.SEL_ALL:
|
||||||
self._makeSelection(QTextCursor.SelectionType.Document)
|
self._makeSelection(QTextCursor.SelectionType.Document)
|
||||||
elif action == nwDocAction.SEL_PARA:
|
elif action == nwDocAction.SEL_PARA:
|
||||||
@@ -783,9 +800,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
elif action == nwDocAction.BLOCK_HSC:
|
elif action == nwDocAction.BLOCK_HSC:
|
||||||
self._formatBlock(nwDocAction.BLOCK_HSC)
|
self._formatBlock(nwDocAction.BLOCK_HSC)
|
||||||
elif action == nwDocAction.REPL_SNG:
|
elif action == nwDocAction.REPL_SNG:
|
||||||
self._replaceQuotes("'", self._typSQuoteO, self._typSQuoteC)
|
self._replaceQuotes("'", tConf.typSQuoteO, tConf.typSQuoteC)
|
||||||
elif action == nwDocAction.REPL_DBL:
|
elif action == nwDocAction.REPL_DBL:
|
||||||
self._replaceQuotes("\"", self._typDQuoteO, self._typDQuoteC)
|
self._replaceQuotes("\"", tConf.typDQuoteO, tConf.typDQuoteC)
|
||||||
elif action == nwDocAction.RM_BREAKS:
|
elif action == nwDocAction.RM_BREAKS:
|
||||||
self._removeInParLineBreaks()
|
self._removeInParLineBreaks()
|
||||||
elif action == nwDocAction.ALIGN_L:
|
elif action == nwDocAction.ALIGN_L:
|
||||||
@@ -857,13 +874,13 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
text = insert
|
text = insert
|
||||||
elif isinstance(insert, nwDocInsert):
|
elif isinstance(insert, nwDocInsert):
|
||||||
if insert == nwDocInsert.QUOTE_LS:
|
if insert == nwDocInsert.QUOTE_LS:
|
||||||
text = self._typSQuoteO
|
text = self._typConf.typSQuoteO
|
||||||
elif insert == nwDocInsert.QUOTE_RS:
|
elif insert == nwDocInsert.QUOTE_RS:
|
||||||
text = self._typSQuoteC
|
text = self._typConf.typSQuoteC
|
||||||
elif insert == nwDocInsert.QUOTE_LD:
|
elif insert == nwDocInsert.QUOTE_LD:
|
||||||
text = self._typDQuoteO
|
text = self._typConf.typDQuoteO
|
||||||
elif insert == nwDocInsert.QUOTE_RD:
|
elif insert == nwDocInsert.QUOTE_RD:
|
||||||
text = self._typDQuoteC
|
text = self._typConf.typDQuoteC
|
||||||
elif insert == nwDocInsert.SYNOPSIS:
|
elif insert == nwDocInsert.SYNOPSIS:
|
||||||
text = "%Synopsis: "
|
text = "%Synopsis: "
|
||||||
block = True
|
block = True
|
||||||
@@ -1978,88 +1995,98 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
if tLen < 1 or tPos-1 > tLen:
|
if tLen < 1 or tPos-1 > tLen:
|
||||||
return
|
return
|
||||||
|
|
||||||
tOne = text[tPos-1:tPos]
|
t1 = text[tPos-1:tPos]
|
||||||
tTwo = text[tPos-2:tPos]
|
t2 = text[tPos-2:tPos]
|
||||||
tThree = text[tPos-3:tPos]
|
t3 = text[tPos-3:tPos]
|
||||||
|
t4 = text[tPos-4:tPos]
|
||||||
|
|
||||||
if not tOne:
|
if not t1:
|
||||||
return
|
return
|
||||||
|
|
||||||
nDelete = 0
|
delete = 0
|
||||||
tInsert = tOne
|
insert = t1
|
||||||
|
tConf = self._typConf
|
||||||
|
|
||||||
if self._typRepDQuote and tTwo[:1].isspace() and tTwo.endswith('"'):
|
if tConf.typRepDQuote and t2[:1].isspace() and t2.endswith('"'):
|
||||||
nDelete = 1
|
delete = 1
|
||||||
tInsert = self._typDQuoteO
|
insert = tConf.typDQuoteO
|
||||||
|
|
||||||
elif self._typRepDQuote and tOne == '"':
|
elif tConf.typRepDQuote and t1 == '"':
|
||||||
nDelete = 1
|
delete = 1
|
||||||
if tPos == 1:
|
if tPos == 1:
|
||||||
tInsert = self._typDQuoteO
|
insert = tConf.typDQuoteO
|
||||||
elif tPos == 2 and tTwo == '>"':
|
elif tPos == 2 and t2 == '>"':
|
||||||
tInsert = self._typDQuoteO
|
insert = tConf.typDQuoteO
|
||||||
elif tPos == 3 and tThree == '>>"':
|
elif tPos == 3 and t3 == '>>"':
|
||||||
tInsert = self._typDQuoteO
|
insert = tConf.typDQuoteO
|
||||||
else:
|
else:
|
||||||
tInsert = self._typDQuoteC
|
insert = tConf.typDQuoteC
|
||||||
|
|
||||||
elif self._typRepSQuote and tTwo[:1].isspace() and tTwo.endswith("'"):
|
elif tConf.typRepSQuote and t2[:1].isspace() and t2.endswith("'"):
|
||||||
nDelete = 1
|
delete = 1
|
||||||
tInsert = self._typSQuoteO
|
insert = tConf.typSQuoteO
|
||||||
|
|
||||||
elif self._typRepSQuote and tOne == "'":
|
elif tConf.typRepSQuote and t1 == "'":
|
||||||
nDelete = 1
|
delete = 1
|
||||||
if tPos == 1:
|
if tPos == 1:
|
||||||
tInsert = self._typSQuoteO
|
insert = tConf.typSQuoteO
|
||||||
elif tPos == 2 and tTwo == ">'":
|
elif tPos == 2 and t2 == ">'":
|
||||||
tInsert = self._typSQuoteO
|
insert = tConf.typSQuoteO
|
||||||
elif tPos == 3 and tThree == ">>'":
|
elif tPos == 3 and t3 == ">>'":
|
||||||
tInsert = self._typSQuoteO
|
insert = tConf.typSQuoteO
|
||||||
else:
|
else:
|
||||||
tInsert = self._typSQuoteC
|
insert = tConf.typSQuoteC
|
||||||
|
|
||||||
elif self._typRepDash and tThree == "---":
|
elif tConf.typRepDash and t4 == "----":
|
||||||
nDelete = 3
|
delete = 4
|
||||||
tInsert = nwUnicode.U_EMDASH
|
insert = nwUnicode.U_HBAR
|
||||||
|
|
||||||
elif self._typRepDash and tTwo == "--":
|
elif tConf.typRepDash and t3 == "---":
|
||||||
nDelete = 2
|
delete = 3
|
||||||
tInsert = nwUnicode.U_ENDASH
|
insert = nwUnicode.U_EMDASH
|
||||||
|
|
||||||
elif self._typRepDash and tTwo == nwUnicode.U_ENDASH + "-":
|
elif tConf.typRepDash and t2 == "--":
|
||||||
nDelete = 2
|
delete = 2
|
||||||
tInsert = nwUnicode.U_EMDASH
|
insert = nwUnicode.U_ENDASH
|
||||||
|
|
||||||
elif self._typRepDots and tThree == "...":
|
elif tConf.typRepDash and t2 == nwUnicode.U_ENDASH + "-":
|
||||||
nDelete = 3
|
delete = 2
|
||||||
tInsert = nwUnicode.U_HELLIP
|
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
|
# This resolves issue #1150
|
||||||
nDelete = 1
|
delete = 1
|
||||||
tInsert = nwUnicode.U_PSEP
|
insert = nwUnicode.U_PSEP
|
||||||
|
|
||||||
tCheck = tInsert
|
check = insert
|
||||||
if self._typPadBefore and tCheck in self._typPadBefore:
|
if tConf.typPadBefore and check in tConf.typPadBefore:
|
||||||
if self._allowSpaceBeforeColon(text, tCheck):
|
if self._allowSpaceBeforeColon(text, check):
|
||||||
nDelete = max(nDelete, 1)
|
delete = max(delete, 1)
|
||||||
chkPos = tPos - nDelete - 1
|
chkPos = tPos - delete - 1
|
||||||
if chkPos >= 0 and text[chkPos].isspace():
|
if chkPos >= 0 and text[chkPos].isspace():
|
||||||
# Strip existing space before inserting a new (#1061)
|
# Strip existing space before inserting a new (#1061)
|
||||||
nDelete += 1
|
delete += 1
|
||||||
tInsert = self._typPadChar + tInsert
|
insert = tConf.typPadChar + insert
|
||||||
|
|
||||||
if self._typPadAfter and tCheck in self._typPadAfter:
|
if tConf.typPadAfter and check in tConf.typPadAfter:
|
||||||
if self._allowSpaceBeforeColon(text, tCheck):
|
if self._allowSpaceBeforeColon(text, check):
|
||||||
nDelete = max(nDelete, 1)
|
delete = max(delete, 1)
|
||||||
tInsert = tInsert + self._typPadChar
|
insert = insert + tConf.typPadChar
|
||||||
|
|
||||||
if nDelete > 0:
|
if delete > 0:
|
||||||
cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete)
|
cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
|
||||||
cursor.insertText(tInsert)
|
cursor.insertText(insert)
|
||||||
|
|
||||||
# Re-highlight, since the auto-replace sometimes interferes with it
|
# Re-highlight, since the auto-replace sometimes interferes with it
|
||||||
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ from novelwriter.common import checkInt
|
|||||||
from novelwriter.constants import nwStyles, nwUnicode
|
from novelwriter.constants import nwStyles, nwUnicode
|
||||||
from novelwriter.core.index import processComment
|
from novelwriter.core.index import processComment
|
||||||
from novelwriter.enum import nwComment
|
from novelwriter.enum import nwComment
|
||||||
from novelwriter.text.patterns import REGEX_PATTERNS
|
from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -59,8 +59,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"_tHandle", "_isNovel", "_isInactive", "_spellCheck", "_spellErr",
|
"_tHandle", "_isNovel", "_isInactive", "_spellCheck", "_spellErr",
|
||||||
"_hStyles", "_minRules", "_txtRules", "_cmnRules", "_dialogLine",
|
"_hStyles", "_minRules", "_txtRules", "_cmnRules", "_dialogParser",
|
||||||
"_narratorBreak",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, document: QTextDocument) -> None:
|
def __init__(self, document: QTextDocument) -> None:
|
||||||
@@ -79,8 +78,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
self._txtRules: list[tuple[re.Pattern, dict[int, QTextCharFormat]]] = []
|
self._txtRules: list[tuple[re.Pattern, dict[int, QTextCharFormat]]] = []
|
||||||
self._cmnRules: list[tuple[re.Pattern, dict[int, QTextCharFormat]]] = []
|
self._cmnRules: list[tuple[re.Pattern, dict[int, QTextCharFormat]]] = []
|
||||||
|
|
||||||
self._dialogLine = ""
|
self._dialogParser = DialogParser()
|
||||||
self._narratorBreak = ""
|
|
||||||
|
|
||||||
self.initHighlighter()
|
self.initHighlighter()
|
||||||
|
|
||||||
@@ -136,8 +134,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
self._txtRules.clear()
|
self._txtRules.clear()
|
||||||
self._cmnRules.clear()
|
self._cmnRules.clear()
|
||||||
|
|
||||||
self._dialogLine = CONFIG.dialogLine.strip()[:1]
|
self._dialogParser.initParser()
|
||||||
self._narratorBreak = CONFIG.narratorBreak.strip()[:1]
|
|
||||||
|
|
||||||
# Multiple or Trailing Spaces
|
# Multiple or Trailing Spaces
|
||||||
if CONFIG.showMultiSpaces:
|
if CONFIG.showMultiSpaces:
|
||||||
@@ -158,16 +155,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
self._txtRules.append((rxRule, hlRule))
|
self._txtRules.append((rxRule, hlRule))
|
||||||
self._cmnRules.append((rxRule, hlRule))
|
self._cmnRules.append((rxRule, hlRule))
|
||||||
|
|
||||||
# Dialogue
|
# Alt Dialogue
|
||||||
if CONFIG.dialogStyle > 0:
|
if rxRule := REGEX_PATTERNS.altDialogStyle:
|
||||||
rxRule = REGEX_PATTERNS.dialogStyle
|
|
||||||
hlRule = {
|
|
||||||
0: self._hStyles["dialog"],
|
|
||||||
}
|
|
||||||
self._txtRules.append((rxRule, hlRule))
|
|
||||||
|
|
||||||
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
|
||||||
rxRule = REGEX_PATTERNS.altDialogStyle
|
|
||||||
hlRule = {
|
hlRule = {
|
||||||
0: self._hStyles["altdialog"],
|
0: self._hStyles["altdialog"],
|
||||||
}
|
}
|
||||||
@@ -403,17 +392,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
else: # Text Paragraph
|
else: # Text Paragraph
|
||||||
self.setCurrentBlockState(BLOCK_TEXT)
|
self.setCurrentBlockState(BLOCK_TEXT)
|
||||||
hRules = self._txtRules if self._isNovel else self._minRules
|
hRules = self._txtRules if self._isNovel else self._minRules
|
||||||
|
if self._dialogParser.enabled:
|
||||||
if self._dialogLine and text.startswith(self._dialogLine):
|
for pos, end in self._dialogParser(text):
|
||||||
if self._narratorBreak:
|
length = end - pos
|
||||||
tPos = 0
|
self.setFormat(pos, length, self._hStyles["dialog"])
|
||||||
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:
|
if hRules:
|
||||||
for rX, hRule in hRules:
|
for rX, hRule in hRules:
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
sPos = self.verticalScrollBar().value()
|
sPos = self.verticalScrollBar().value()
|
||||||
qDoc = ToQTextDocument(SHARED.project)
|
qDoc = ToQTextDocument(SHARED.project)
|
||||||
qDoc.setJustify(CONFIG.doJustify)
|
qDoc.setJustify(CONFIG.doJustify)
|
||||||
qDoc.setDialogueHighlight(True)
|
qDoc.setDialogHighlight(True)
|
||||||
qDoc.setFont(CONFIG.textFont)
|
qDoc.setFont(CONFIG.textFont)
|
||||||
qDoc.setTheme(self._docTheme)
|
qDoc.setTheme(self._docTheme)
|
||||||
qDoc.initDocument()
|
qDoc.initDocument()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
|
from novelwriter.common import compact, uniqueCompact
|
||||||
from novelwriter.constants import nwRegEx
|
from novelwriter.constants import nwRegEx
|
||||||
|
|
||||||
|
|
||||||
@@ -82,26 +83,103 @@ class RegExPatterns:
|
|||||||
return self._rxSCValue
|
return self._rxSCValue
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dialogStyle(self) -> re.Pattern:
|
def dialogStyle(self) -> re.Pattern | None:
|
||||||
"""Dialogue detection rule based on user settings."""
|
"""Dialogue detection rule based on user settings."""
|
||||||
symO = ""
|
if CONFIG.dialogStyle > 0:
|
||||||
symC = ""
|
symO = ""
|
||||||
if CONFIG.dialogStyle in (1, 3):
|
symC = ""
|
||||||
symO += CONFIG.fmtSQuoteOpen
|
if CONFIG.dialogStyle in (1, 3):
|
||||||
symC += CONFIG.fmtSQuoteClose
|
symO += CONFIG.fmtSQuoteOpen.strip()[:1]
|
||||||
if CONFIG.dialogStyle in (2, 3):
|
symC += CONFIG.fmtSQuoteClose.strip()[:1]
|
||||||
symO += CONFIG.fmtDQuoteOpen
|
if CONFIG.dialogStyle in (2, 3):
|
||||||
symC += CONFIG.fmtDQuoteClose
|
symO += CONFIG.fmtDQuoteOpen.strip()[:1]
|
||||||
|
symC += CONFIG.fmtDQuoteClose.strip()[:1]
|
||||||
|
|
||||||
rxEnd = "|$" if CONFIG.allowOpenDial else ""
|
rxEnd = "|$" if CONFIG.allowOpenDial else ""
|
||||||
return re.compile(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})", re.UNICODE)
|
return re.compile(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})", re.UNICODE)
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def altDialogStyle(self) -> re.Pattern:
|
def altDialogStyle(self) -> re.Pattern | None:
|
||||||
"""Dialogue alternative rule based on user settings."""
|
"""Dialogue alternative rule based on user settings."""
|
||||||
symO = re.escape(CONFIG.altDialogOpen)
|
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
||||||
symC = re.escape(CONFIG.altDialogClose)
|
symO = re.escape(compact(CONFIG.altDialogOpen))
|
||||||
return re.compile(f"\\B{symO}.*?{symC}\\B", re.UNICODE)
|
symC = re.escape(compact(CONFIG.altDialogClose))
|
||||||
|
return re.compile(f"\\B{symO}.*?{symC}\\B", re.UNICODE)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
REGEX_PATTERNS = RegExPatterns()
|
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
|
%%~name: New Scene
|
||||||
%%~path: 000000000000d/000000000000f
|
%%~path: 000000000000d/000000000000f
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
%%~hash: 89b54ddaec2fddfd220e91dd438c84fb3aef9fc2
|
%%~hash: e4148ea77e78c90c334d5dc46c38a2b7904ac117
|
||||||
%%~date: 2024-10-30 00:07:21/2024-10-30 00:07:26
|
%%~date: 2024-11-01 21:15:57/2024-11-01 21:16:01
|
||||||
# Novel
|
# Novel
|
||||||
|
|
||||||
## Chapter
|
## Chapter
|
||||||
@@ -26,7 +26,7 @@ This is a paragraph of nonsense text.
|
|||||||
This is another paragraph
|
This is another paragraph
|
||||||
with a line separator in it.
|
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.”
|
“Full line double quoted text.”
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?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">
|
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="5">
|
||||||
<name>New Project</name>
|
<name>New Project</name>
|
||||||
<author>Jane Doe</author>
|
<author>Jane Doe</author>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<entry key="i000007" count="0" red="50" green="200" blue="0" shape="SQUARE">Main</entry>
|
<entry key="i000007" count="0" red="50" green="200" blue="0" shape="SQUARE">Main</entry>
|
||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</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">
|
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
|
||||||
<meta expanded="yes" />
|
<meta expanded="yes" />
|
||||||
<name status="s000000" import="i000004">Novel</name>
|
<name status="s000000" import="i000004">Novel</name>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<name status="s000000" import="i000004" active="yes">New Chapter</name>
|
<name status="s000000" import="i000004" active="yes">New Chapter</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<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>
|
<name status="s000000" import="i000004" active="yes">New Scene</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
|
|||||||
project = NWProject()
|
project = NWProject()
|
||||||
html = ToHtml(project)
|
html = ToHtml(project)
|
||||||
html.initDocument()
|
html.initDocument()
|
||||||
|
|
||||||
html._isNovel = True
|
html._isNovel = True
|
||||||
html._isFirst = 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"
|
"<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._text = "## Chapter\n\nThis text \u201chas dialogue\u201d in it.\n\n"
|
||||||
html.tokenizeText()
|
html.tokenizeText()
|
||||||
html.doConvert()
|
html.doConvert()
|
||||||
assert html._pages[-1] == (
|
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"
|
"<p>This text <span style='color: #4271ae'>“has dialogue”</span> in it.</p>\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Alt. Dialogue
|
# Alt Dialog
|
||||||
CONFIG.altDialogOpen = "::"
|
|
||||||
CONFIG.altDialogClose = "::"
|
|
||||||
html.setDialogueHighlight(True)
|
|
||||||
html._text = "## Chapter\n\nThis text ::has alt dialogue:: in it.\n\n"
|
html._text = "## Chapter\n\nThis text ::has alt dialogue:: in it.\n\n"
|
||||||
html.tokenizeText()
|
html.tokenizeText()
|
||||||
html.doConvert()
|
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"
|
"<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 = (
|
html._text = (
|
||||||
"Text with one[footnote:fa] or two[footnote:fb] footnotes.\n\n"
|
"Text with one[footnote:fa] or two[footnote:fb] footnotes.\n\n"
|
||||||
|
|||||||
@@ -1286,7 +1286,7 @@ def testFmtToken_Dialogue(mockGUI):
|
|||||||
|
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
tokens = BareTokenizer(project)
|
tokens = BareTokenizer(project)
|
||||||
tokens.setDialogueHighlight(True)
|
tokens.setDialogHighlight(True)
|
||||||
tokens._handle = TMH
|
tokens._handle = TMH
|
||||||
tokens._isNovel = True
|
tokens._isNovel = True
|
||||||
|
|
||||||
@@ -1337,7 +1337,10 @@ def testFmtToken_Dialogue(mockGUI):
|
|||||||
|
|
||||||
# Dialogue line
|
# Dialogue line
|
||||||
CONFIG.dialogLine = "\u2013"
|
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._text = "\u2013 Dialogue line without narrator break.\n"
|
||||||
tokens.tokenizeText()
|
tokens.tokenizeText()
|
||||||
assert tokens._blocks == [(
|
assert tokens._blocks == [(
|
||||||
@@ -1352,16 +1355,19 @@ def testFmtToken_Dialogue(mockGUI):
|
|||||||
|
|
||||||
# Dialogue line with narrator break
|
# Dialogue line with narrator break
|
||||||
CONFIG.narratorBreak = "\u2013"
|
CONFIG.narratorBreak = "\u2013"
|
||||||
tokens.setDialogueHighlight(True)
|
tokens = BareTokenizer(project)
|
||||||
tokens._text = "\u2013 Dialogue with a narrator break, \u2013he said,\u2013 see?\n"
|
tokens.setDialogHighlight(True)
|
||||||
|
tokens._handle = TMH
|
||||||
|
tokens._isNovel = True
|
||||||
|
tokens._text = "\u2013 Dialogue with a narrator break, \u2013he said\u2013, see?\n"
|
||||||
tokens.tokenizeText()
|
tokens.tokenizeText()
|
||||||
assert tokens._blocks == [(
|
assert tokens._blocks == [(
|
||||||
BlockTyp.TEXT, "",
|
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"),
|
(0, TextFmt.COL_B, "dialog"),
|
||||||
(34, TextFmt.COL_E, ""),
|
(34, TextFmt.COL_E, ""),
|
||||||
(43, TextFmt.COL_B, "dialog"),
|
(44, TextFmt.COL_B, "dialog"),
|
||||||
(49, TextFmt.COL_E, ""),
|
(49, TextFmt.COL_E, ""),
|
||||||
],
|
],
|
||||||
BlockFmt.NONE
|
BlockFmt.NONE
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ def testFmtToOdt_DialogueFormatting(mockGUI):
|
|||||||
"""Test formatting of dialogue."""
|
"""Test formatting of dialogue."""
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
odt = ToOdt(project, isFlat=True)
|
odt = ToOdt(project, isFlat=True)
|
||||||
odt.setDialogueHighlight(True)
|
odt.setDialogHighlight(True)
|
||||||
odt.initDocument()
|
odt.initDocument()
|
||||||
oStyle = ODTParagraphStyle("test")
|
oStyle = ODTParagraphStyle("test")
|
||||||
|
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ def testFmtToQTextDocument_TextCharFormats(mockGUI):
|
|||||||
|
|
||||||
# Convert before init
|
# Convert before init
|
||||||
doc._text = "Blabla"
|
doc._text = "Blabla"
|
||||||
doc.setDialogueHighlight(True)
|
doc.setDialogHighlight(True)
|
||||||
doc.doConvert()
|
doc.doConvert()
|
||||||
doc.tokenizeText()
|
doc.tokenizeText()
|
||||||
assert doc.document.toPlainText() == ""
|
assert doc.document.toPlainText() == ""
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
|||||||
assert qDoc.defaultTextOption().alignment() == QtAlignLeft
|
assert qDoc.defaultTextOption().alignment() == QtAlignLeft
|
||||||
assert docEditor.verticalScrollBarPolicy() == QtScrollAsNeeded
|
assert docEditor.verticalScrollBarPolicy() == QtScrollAsNeeded
|
||||||
assert docEditor.horizontalScrollBarPolicy() == QtScrollAsNeeded
|
assert docEditor.horizontalScrollBarPolicy() == QtScrollAsNeeded
|
||||||
assert docEditor._typPadChar == nwUnicode.U_NBSP
|
assert docEditor._typConf.typPadChar == nwUnicode.U_NBSP
|
||||||
assert docEditor.docHeader.itemTitle.text() == (
|
assert docEditor.docHeader.itemTitle.text() == (
|
||||||
"Novel \u203a New Chapter \u203a New Scene"
|
"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 qDoc.defaultTextOption().flags() & QTextOption.ShowLineAndParagraphSeparators
|
||||||
assert docEditor.verticalScrollBarPolicy() == QtScrollAlwaysOff
|
assert docEditor.verticalScrollBarPolicy() == QtScrollAlwaysOff
|
||||||
assert docEditor.horizontalScrollBarPolicy() == 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"
|
assert docEditor.docHeader.itemTitle.text() == "New Scene"
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
|
|||||||
@@ -421,6 +421,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ":
|
for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ":
|
||||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
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 ... ":
|
for c in "Ellipsis? Not a problem either ... ":
|
||||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
for c in "How about three hyphens - -":
|
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_Left, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(docEditor, Qt.Key.Key_Backspace, 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 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, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key.Key_Return, 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)
|
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
# Insert spaces before and after quotes
|
# Insert spaces before and after quotes
|
||||||
docEditor._typPadBefore = "\u201d"
|
CONFIG.fmtPadBefore = "\u201d"
|
||||||
docEditor._typPadAfter = "\u201c"
|
CONFIG.fmtPadAfter = "\u201c"
|
||||||
|
docEditor.initEditor()
|
||||||
|
|
||||||
for c in "Some \"double quoted text with spaces padded\".":
|
for c in "Some \"double quoted text with spaces padded\".":
|
||||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
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)
|
||||||
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._typPadAfter = ""
|
CONFIG.fmtPadAfter = ""
|
||||||
|
docEditor.initEditor()
|
||||||
|
|
||||||
# Dialogue Line
|
# Dialogue Line
|
||||||
for c in "-- Hi, I am a character speaking.":
|
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
|
# Insert spaces before colon, but ignore tags
|
||||||
docEditor._typPadBefore = ":"
|
CONFIG.fmtPadBefore = ":"
|
||||||
|
docEditor.initEditor()
|
||||||
|
|
||||||
for c in "@object: NoSpaceAdded":
|
for c in "@object: NoSpaceAdded":
|
||||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
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)
|
||||||
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
|
# Indent and Align
|
||||||
# ================
|
# ================
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import pytest
|
|||||||
|
|
||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
from novelwriter.constants import nwUnicode
|
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]]:
|
def allMatches(regEx: re.Pattern, text: str) -> list[list[str]]:
|
||||||
@@ -267,6 +267,10 @@ def testTextPatterns_ShortcodesValue():
|
|||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testTextPatterns_DialogueStyle():
|
def testTextPatterns_DialogueStyle():
|
||||||
"""Test the dialogue style pattern regexes."""
|
"""Test the dialogue style pattern regexes."""
|
||||||
|
# Before set, the regex is None
|
||||||
|
CONFIG.dialogStyle = 0
|
||||||
|
assert REGEX_PATTERNS.dialogStyle is None
|
||||||
|
|
||||||
# Set the config
|
# Set the config
|
||||||
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
|
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
|
||||||
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
|
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
|
||||||
@@ -280,6 +284,7 @@ def testTextPatterns_DialogueStyle():
|
|||||||
|
|
||||||
CONFIG.allowOpenDial = False
|
CONFIG.allowOpenDial = False
|
||||||
regEx = REGEX_PATTERNS.dialogStyle
|
regEx = REGEX_PATTERNS.dialogStyle
|
||||||
|
assert regEx is not None
|
||||||
|
|
||||||
# Defined single quotes are recognised
|
# Defined single quotes are recognised
|
||||||
assert allMatches(regEx, "one \u2018two\u2019 three") == [
|
assert allMatches(regEx, "one \u2018two\u2019 three") == [
|
||||||
@@ -305,6 +310,7 @@ def testTextPatterns_DialogueStyle():
|
|||||||
|
|
||||||
CONFIG.allowOpenDial = True
|
CONFIG.allowOpenDial = True
|
||||||
regEx = REGEX_PATTERNS.dialogStyle
|
regEx = REGEX_PATTERNS.dialogStyle
|
||||||
|
assert regEx is not None
|
||||||
|
|
||||||
# Defined single quotes are recognised also when open
|
# Defined single quotes are recognised also when open
|
||||||
assert allMatches(regEx, "one \u2018two three") == [
|
assert allMatches(regEx, "one \u2018two three") == [
|
||||||
@@ -320,19 +326,19 @@ def testTextPatterns_DialogueStyle():
|
|||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testTextPatterns_DialogueSpecial():
|
def testTextPatterns_DialogueSpecial():
|
||||||
"""Test the special dialogue style pattern regexes."""
|
"""Test the special dialogue style pattern regexes."""
|
||||||
# Set the config
|
# Before set, the regex is None
|
||||||
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
|
CONFIG.altDialogOpen = ""
|
||||||
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
|
CONFIG.altDialogClose = ""
|
||||||
CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO
|
assert REGEX_PATTERNS.altDialogStyle is None
|
||||||
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
|
|
||||||
|
|
||||||
CONFIG.dialogStyle = 3
|
# Set the config
|
||||||
CONFIG.altDialogOpen = "::"
|
CONFIG.altDialogOpen = "::"
|
||||||
CONFIG.altDialogClose = "::"
|
CONFIG.altDialogClose = "::"
|
||||||
|
|
||||||
# Alternative Dialogue
|
# Alternative Dialogue
|
||||||
# ====================
|
# ====================
|
||||||
regEx = REGEX_PATTERNS.altDialogStyle
|
regEx = REGEX_PATTERNS.altDialogStyle
|
||||||
|
assert regEx is not None
|
||||||
|
|
||||||
# With no padding
|
# With no padding
|
||||||
assert allMatches(regEx, "one ::two:: three") == [
|
assert allMatches(regEx, "one ::two:: three") == [
|
||||||
@@ -343,3 +349,106 @@ def testTextPatterns_DialogueSpecial():
|
|||||||
assert allMatches(regEx, "one :: two :: three") == [
|
assert allMatches(regEx, "one :: two :: three") == [
|
||||||
[(":: two ::", 4, 13)]
|
[(":: 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