Extend dialogue highlighting (#1908)
This commit is contained in:
@@ -61,10 +61,10 @@ class nwConst:
|
||||
class nwRegEx:
|
||||
|
||||
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:i|b|s|u|m|sup|sub)\])"
|
||||
FMT_SV = r"(?<!\\)(\[(?i)(?:footnote):)(.+?)(?<!\\)(\])"
|
||||
FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])"
|
||||
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
|
||||
|
||||
|
||||
class nwShortcode:
|
||||
|
||||
@@ -82,6 +82,7 @@ SETTINGS_TEMPLATE = {
|
||||
"format.stripUnicode": (bool, False),
|
||||
"format.replaceTabs": (bool, False),
|
||||
"format.keepBreaks": (bool, True),
|
||||
"format.showDialogue": (bool, False),
|
||||
"format.firstLineIndent": (bool, False),
|
||||
"format.firstIndentWidth": (float, 1.4),
|
||||
"format.indentFirstPar": (bool, False),
|
||||
@@ -131,6 +132,7 @@ SETTINGS_LABELS = {
|
||||
"format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"),
|
||||
"format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"),
|
||||
"format.keepBreaks": QT_TRANSLATE_NOOP("Builds", "Preserve Hard Line Breaks"),
|
||||
"format.showDialogue": QT_TRANSLATE_NOOP("Builds", "Apply Dialogue Highlighting"),
|
||||
|
||||
"format.grpParIndent": QT_TRANSLATE_NOOP("Builds", "First Line Indent"),
|
||||
"format.firstLineIndent": QT_TRANSLATE_NOOP("Builds", "Enable Indent"),
|
||||
|
||||
@@ -338,6 +338,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.setFirstLineIndent(
|
||||
self._build.getBool("format.firstLineIndent"),
|
||||
self._build.getFloat("format.firstIndentWidth"),
|
||||
|
||||
@@ -52,6 +52,10 @@ HTML5_TAGS = {
|
||||
Tokenizer.FMT_SUP_E: "</sup>",
|
||||
Tokenizer.FMT_SUB_B: "<sub>",
|
||||
Tokenizer.FMT_SUB_E: "</sub>",
|
||||
Tokenizer.FMT_DL_B: "<span class='dialog'>",
|
||||
Tokenizer.FMT_DL_E: "</span>",
|
||||
Tokenizer.FMT_ADL_B: "<span class='altdialog'>",
|
||||
Tokenizer.FMT_ADL_E: "</span>",
|
||||
Tokenizer.FMT_STRIP: "",
|
||||
}
|
||||
|
||||
@@ -431,6 +435,8 @@ class ToHtml(Tokenizer):
|
||||
styles.append(".break {text-align: left;}")
|
||||
styles.append(".synopsis {font-style: italic;}")
|
||||
styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
|
||||
styles.append(".dialog {color: rgb(66, 113, 174);}")
|
||||
styles.append(".altdialog {color: rgb(129, 55, 9);}")
|
||||
|
||||
return styles
|
||||
|
||||
@@ -451,7 +457,7 @@ class ToHtml(Tokenizer):
|
||||
else:
|
||||
html = "<sup>ERR</sup>"
|
||||
else:
|
||||
html = HTML5_TAGS.get(fmt, "ERR")
|
||||
html = HTML5_TAGS.get(fmt, "")
|
||||
temp = f"{temp[:pos]}{html}{temp[pos:]}"
|
||||
temp = temp.replace("\n", "<br>")
|
||||
return stripEscape(temp)
|
||||
|
||||
@@ -36,13 +36,13 @@ from time import time
|
||||
from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.common import checkInt, formatTimeStamp, numberToRoman
|
||||
from novelwriter.constants import (
|
||||
nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst
|
||||
)
|
||||
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst
|
||||
from novelwriter.core.index import processComment
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.enum import nwComment, nwItemLayout
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -85,8 +85,12 @@ class Tokenizer(ABC):
|
||||
FMT_SUP_E = 12 # End superscript
|
||||
FMT_SUB_B = 13 # Begin subscript
|
||||
FMT_SUB_E = 14 # End subscript
|
||||
FMT_FNOTE = 15 # Footnote marker
|
||||
FMT_STRIP = 16 # Strip the format code
|
||||
FMT_DL_B = 15 # Begin dialogue
|
||||
FMT_DL_E = 16 # End dialogue
|
||||
FMT_ADL_B = 17 # Begin alt dialogue
|
||||
FMT_ADL_E = 18 # End alt dialogue
|
||||
FMT_FNOTE = 19 # Footnote marker
|
||||
FMT_STRIP = 20 # Strip the format code
|
||||
|
||||
# Block Type
|
||||
T_EMPTY = 1 # Empty line (new paragraph)
|
||||
@@ -193,7 +197,8 @@ class Tokenizer(ABC):
|
||||
|
||||
# Instance Variables
|
||||
self._hFormatter = HeadingFormatter(self._project)
|
||||
self._noSep = True # Flag to indicate that we don't want a scene separator
|
||||
self._noSep = True # Flag to indicate that we don't want a scene separator
|
||||
self._showDialog = False # Flag for dialogue highlighting
|
||||
|
||||
# This File
|
||||
self._isNovel = False # Document is a novel document
|
||||
@@ -208,12 +213,12 @@ class Tokenizer(ABC):
|
||||
|
||||
# Format RegEx
|
||||
self._rxMarkdown = [
|
||||
(QRegularExpression(nwRegEx.FMT_EI), [0, self.FMT_I_B, 0, self.FMT_I_E]),
|
||||
(QRegularExpression(nwRegEx.FMT_EB), [0, self.FMT_B_B, 0, self.FMT_B_E]),
|
||||
(QRegularExpression(nwRegEx.FMT_ST), [0, self.FMT_D_B, 0, self.FMT_D_E]),
|
||||
(REGEX_PATTERNS.markdownItalic, [0, self.FMT_I_B, 0, self.FMT_I_E]),
|
||||
(REGEX_PATTERNS.markdownBold, [0, self.FMT_B_B, 0, self.FMT_B_E]),
|
||||
(REGEX_PATTERNS.markdownStrike, [0, self.FMT_D_B, 0, self.FMT_D_E]),
|
||||
]
|
||||
self._rxShortCodes = QRegularExpression(nwRegEx.FMT_SC)
|
||||
self._rxShortCodeVals = QRegularExpression(nwRegEx.FMT_SV)
|
||||
self._rxShortCodes = REGEX_PATTERNS.shortcodePlain
|
||||
self._rxShortCodeVals = REGEX_PATTERNS.shortcodeValue
|
||||
|
||||
self._shortCodeFmt = {
|
||||
nwShortcode.ITALIC_O: self.FMT_I_B, nwShortcode.ITALIC_C: self.FMT_I_E,
|
||||
@@ -228,6 +233,8 @@ class Tokenizer(ABC):
|
||||
nwShortcode.FOOTNOTE_B: self.FMT_FNOTE,
|
||||
}
|
||||
|
||||
self._rxDialogue: list[tuple[QRegularExpression, int, int]] = []
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -349,6 +356,29 @@ class Tokenizer(ABC):
|
||||
self._doJustify = state
|
||||
return
|
||||
|
||||
def setDialogueHighlight(self, state: bool) -> None:
|
||||
"""Enable or disable dialogue highlighting."""
|
||||
self._rxDialogue = []
|
||||
self._showDialog = state
|
||||
if state:
|
||||
if CONFIG.dialogStyle > 0:
|
||||
self._rxDialogue.append((
|
||||
REGEX_PATTERNS.dialogStyle, self.FMT_DL_B, self.FMT_DL_E
|
||||
))
|
||||
if CONFIG.dialogLine:
|
||||
self._rxDialogue.append((
|
||||
REGEX_PATTERNS.dialogLine, self.FMT_DL_B, self.FMT_DL_E
|
||||
))
|
||||
if CONFIG.narratorBreak:
|
||||
self._rxDialogue.append((
|
||||
REGEX_PATTERNS.narratorBreak, self.FMT_DL_E, self.FMT_DL_B
|
||||
))
|
||||
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
||||
self._rxDialogue.append((
|
||||
REGEX_PATTERNS.altDialogStyle, self.FMT_ADL_B, self.FMT_ADL_E
|
||||
))
|
||||
return
|
||||
|
||||
def setTitleMargins(self, upper: float, lower: float) -> None:
|
||||
"""Set the upper and lower title margin."""
|
||||
self._marginTitle = (float(upper), float(lower))
|
||||
@@ -1106,6 +1136,15 @@ class Tokenizer(ABC):
|
||||
f"{tHandle}:{rxMatch.captured(2)}",
|
||||
))
|
||||
|
||||
# Match Dialogue
|
||||
if self._rxDialogue:
|
||||
for regEx, fmtB, fmtE in self._rxDialogue:
|
||||
rxItt = regEx.globalMatch(text, 0)
|
||||
while rxItt.hasNext():
|
||||
rxMatch = rxItt.next()
|
||||
temp.append((rxMatch.capturedStart(0), 0, fmtB, ""))
|
||||
temp.append((rxMatch.capturedEnd(0), 0, fmtE, ""))
|
||||
|
||||
# Post-process text and format
|
||||
result = text
|
||||
formats = []
|
||||
|
||||
+45
-14
@@ -82,13 +82,15 @@ TAG_SPAN = _mkTag("text", "span")
|
||||
TAG_STNM = _mkTag("text", "style-name")
|
||||
|
||||
# Formatting Codes
|
||||
X_BLD = 0x01 # Bold format
|
||||
X_ITA = 0x02 # Italic format
|
||||
X_DEL = 0x04 # Strikethrough format
|
||||
X_UND = 0x08 # Underline format
|
||||
X_MRK = 0x10 # Marked format
|
||||
X_SUP = 0x20 # Superscript
|
||||
X_SUB = 0x40 # Subscript
|
||||
X_BLD = 0x001 # Bold format
|
||||
X_ITA = 0x002 # Italic format
|
||||
X_DEL = 0x004 # Strikethrough format
|
||||
X_UND = 0x008 # Underline format
|
||||
X_MRK = 0x010 # Marked format
|
||||
X_SUP = 0x020 # Superscript
|
||||
X_SUB = 0x040 # Subscript
|
||||
X_DLG = 0x080 # Dialogue
|
||||
X_DLA = 0x100 # Alt. Dialogue
|
||||
|
||||
# Formatting Masks
|
||||
M_BLD = ~X_BLD
|
||||
@@ -98,6 +100,8 @@ M_UND = ~X_UND
|
||||
M_MRK = ~X_MRK
|
||||
M_SUP = ~X_SUP
|
||||
M_SUB = ~X_SUB
|
||||
M_DLG = ~X_DLG
|
||||
M_DLA = ~X_DLA
|
||||
|
||||
# ODT Styles
|
||||
S_TITLE = "Title"
|
||||
@@ -216,13 +220,15 @@ class ToOdt(Tokenizer):
|
||||
self._mDocRight = "2.000cm"
|
||||
|
||||
# Colour
|
||||
self._colHead12 = None
|
||||
self._opaHead12 = None
|
||||
self._colHead34 = None
|
||||
self._opaHead34 = None
|
||||
self._colMetaTx = None
|
||||
self._opaMetaTx = None
|
||||
self._markText = "#ffffa6"
|
||||
self._colHead12 = None
|
||||
self._opaHead12 = None
|
||||
self._colHead34 = None
|
||||
self._opaHead34 = None
|
||||
self._colMetaTx = None
|
||||
self._opaMetaTx = None
|
||||
self._colDialogM = None
|
||||
self._colDialogA = None
|
||||
self._markText = "#ffffa6"
|
||||
|
||||
return
|
||||
|
||||
@@ -324,6 +330,10 @@ class ToOdt(Tokenizer):
|
||||
self._colMetaTx = "#813709"
|
||||
self._opaMetaTx = "100%"
|
||||
|
||||
if self._showDialog:
|
||||
self._colDialogM = "#2a6099"
|
||||
self._colDialogA = "#813709"
|
||||
|
||||
self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
|
||||
self._fBlockIndent = self._emToCm(self._blockIndent)
|
||||
self._fTextIndent = self._emToCm(self._firstWidth)
|
||||
@@ -684,6 +694,14 @@ class ToOdt(Tokenizer):
|
||||
xFmt |= X_SUB
|
||||
elif fFmt == self.FMT_SUB_E:
|
||||
xFmt &= M_SUB
|
||||
elif fFmt == self.FMT_DL_B:
|
||||
xFmt |= X_DLG
|
||||
elif fFmt == self.FMT_DL_E:
|
||||
xFmt &= M_DLG
|
||||
elif fFmt == self.FMT_ADL_B:
|
||||
xFmt |= X_DLA
|
||||
elif fFmt == self.FMT_ADL_E:
|
||||
xFmt &= M_DLA
|
||||
elif fFmt == self.FMT_FNOTE:
|
||||
xNode = self._generateFootnote(fData)
|
||||
elif fFmt == self.FMT_STRIP:
|
||||
@@ -757,6 +775,10 @@ class ToOdt(Tokenizer):
|
||||
style.setTextPosition("super")
|
||||
if hFmt & X_SUB:
|
||||
style.setTextPosition("sub")
|
||||
if hFmt & X_DLG:
|
||||
style.setColour(self._colDialogM)
|
||||
if hFmt & X_DLA:
|
||||
style.setColour(self._colDialogA)
|
||||
self._autoText[hFmt] = style
|
||||
|
||||
return style.name
|
||||
@@ -1357,6 +1379,7 @@ class ODTTextStyle:
|
||||
self._tAttr = {
|
||||
"font-weight": ["fo", None],
|
||||
"font-style": ["fo", None],
|
||||
"color": ["fo", None],
|
||||
"background-color": ["fo", None],
|
||||
"text-position": ["style", None],
|
||||
"text-line-through-style": ["style", None],
|
||||
@@ -1391,6 +1414,14 @@ class ODTTextStyle:
|
||||
self._tAttr["font-style"][1] = None
|
||||
return
|
||||
|
||||
def setColour(self, value: str | None) -> None:
|
||||
"""Set text colour."""
|
||||
if value and len(value) == 7 and value[0] == "#":
|
||||
self._tAttr["color"][1] = value
|
||||
else:
|
||||
self._tAttr["color"][1] = None
|
||||
return
|
||||
|
||||
def setBackgroundColour(self, value: str | None) -> None:
|
||||
"""Set text background colour."""
|
||||
if value and len(value) == 7 and value[0] == "#":
|
||||
|
||||
@@ -45,16 +45,18 @@ T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
|
||||
|
||||
|
||||
class TextDocumentTheme:
|
||||
text: QColor = QtBlack
|
||||
text: QColor = QtBlack
|
||||
highlight: QColor = QtTransparent
|
||||
head: QColor = QtBlack
|
||||
comment: QColor = QtBlack
|
||||
note: QColor = QtBlack
|
||||
code: QColor = QtBlack
|
||||
modifier: QColor = QtBlack
|
||||
keyword: QColor = QtBlack
|
||||
tag: QColor = QtBlack
|
||||
optional: QColor = QtBlack
|
||||
head: QColor = QtBlack
|
||||
comment: QColor = QtBlack
|
||||
note: QColor = QtBlack
|
||||
code: QColor = QtBlack
|
||||
modifier: QColor = QtBlack
|
||||
keyword: QColor = QtBlack
|
||||
tag: QColor = QtBlack
|
||||
optional: QColor = QtBlack
|
||||
dialog: QColor = QtBlack
|
||||
altdialog: QColor = QtBlack
|
||||
|
||||
|
||||
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
|
||||
@@ -340,6 +342,14 @@ class ToQTextDocument(Tokenizer):
|
||||
cFmt.setVerticalAlignment(QtVAlignSub)
|
||||
elif fmt == self.FMT_SUB_E:
|
||||
cFmt.setVerticalAlignment(QtVAlignNormal)
|
||||
elif fmt == self.FMT_DL_B:
|
||||
cFmt.setForeground(self._theme.dialog)
|
||||
elif fmt == self.FMT_DL_E:
|
||||
cFmt.setForeground(self._theme.text)
|
||||
elif fmt == self.FMT_ADL_B:
|
||||
cFmt.setForeground(self._theme.altdialog)
|
||||
elif fmt == self.FMT_ADL_E:
|
||||
cFmt.setForeground(self._theme.text)
|
||||
elif fmt == self.FMT_FNOTE:
|
||||
xFmt = QTextCharFormat(self._cCode)
|
||||
xFmt.setVerticalAlignment(QtVAlignSuper)
|
||||
|
||||
@@ -2030,6 +2030,9 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete)
|
||||
cursor.insertText(tInsert)
|
||||
|
||||
# Re-highlight, since the auto-replace sometimes interferes with it
|
||||
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -39,6 +39,7 @@ from novelwriter.common import checkInt
|
||||
from novelwriter.constants import nwHeaders, nwRegEx, nwUnicode
|
||||
from novelwriter.core.index import processComment
|
||||
from novelwriter.enum import nwComment
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS
|
||||
from novelwriter.types import QRegExUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -59,8 +60,8 @@ BLOCK_TITLE = 4
|
||||
class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
__slots__ = (
|
||||
"_tHandle", "_isInactive", "_spellCheck", "_spellErr", "_hStyles",
|
||||
"_txtRules", "_cmnRules",
|
||||
"_tHandle", "_isNovel", "_isInactive", "_spellCheck", "_spellErr",
|
||||
"_hStyles", "_minRules", "_txtRules", "_cmnRules",
|
||||
)
|
||||
|
||||
def __init__(self, document: QTextDocument) -> None:
|
||||
@@ -69,11 +70,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
logger.debug("Create: GuiDocHighlighter")
|
||||
|
||||
self._tHandle = None
|
||||
self._isNovel = False
|
||||
self._isInactive = False
|
||||
self._spellCheck = False
|
||||
self._spellErr = QTextCharFormat()
|
||||
|
||||
self._hStyles: dict[str, QTextCharFormat] = {}
|
||||
self._minRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
|
||||
self._txtRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
|
||||
self._cmnRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
|
||||
|
||||
@@ -137,6 +140,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
hlRule = {
|
||||
0: self._hStyles["mspaces"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
@@ -146,106 +150,89 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
hlRule = {
|
||||
0: self._hStyles["nobreak"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Dialogue
|
||||
if CONFIG.dialogStyle > 0:
|
||||
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
|
||||
|
||||
rxEnd = "|$" if CONFIG.allowOpenDial else ""
|
||||
rxRule = QRegularExpression(f"\\B[{symO}].*?[{symC}]\\B{rxEnd}")
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
rxRule = REGEX_PATTERNS.dialogStyle
|
||||
hlRule = {
|
||||
0: self._hStyles["dialog"],
|
||||
}
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
|
||||
if CONFIG.dialogLine:
|
||||
sym = QRegularExpression.escape(CONFIG.dialogLine)
|
||||
rxRule = QRegularExpression(f"^{sym}.*?$")
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
rxRule = REGEX_PATTERNS.dialogLine
|
||||
hlRule = {
|
||||
0: self._hStyles["dialog"],
|
||||
}
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
|
||||
if CONFIG.narratorBreak:
|
||||
sym = QRegularExpression.escape(CONFIG.narratorBreak)
|
||||
rxRule = QRegularExpression(f"({sym}\\b)(.*?)(\\b{sym})")
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
rxRule = REGEX_PATTERNS.narratorBreak
|
||||
hlRule = {
|
||||
0: self._hStyles["text"],
|
||||
}
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
|
||||
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
||||
symO = QRegularExpression.escape(CONFIG.altDialogOpen)
|
||||
symC = QRegularExpression.escape(CONFIG.altDialogClose)
|
||||
rxRule = QRegularExpression(f"\\B{symO}.*?{symC}\\B")
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
rxRule = REGEX_PATTERNS.altDialogStyle
|
||||
hlRule = {
|
||||
0: self._hStyles["altdialog"],
|
||||
}
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
|
||||
# Markdown Italic
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_EI)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
rxRule = REGEX_PATTERNS.markdownItalic
|
||||
hlRule = {
|
||||
1: self._hStyles["markup"],
|
||||
2: self._hStyles["italic"],
|
||||
3: self._hStyles["markup"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Markdown Bold
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_EB)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
rxRule = REGEX_PATTERNS.markdownBold
|
||||
hlRule = {
|
||||
1: self._hStyles["markup"],
|
||||
2: self._hStyles["bold"],
|
||||
3: self._hStyles["markup"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Markdown Strikethrough
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_ST)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
rxRule = REGEX_PATTERNS.markdownStrike
|
||||
hlRule = {
|
||||
1: self._hStyles["markup"],
|
||||
2: self._hStyles["strike"],
|
||||
3: self._hStyles["markup"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Shortcodes
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_SC)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
rxRule = REGEX_PATTERNS.shortcodePlain
|
||||
hlRule = {
|
||||
1: self._hStyles["code"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Shortcodes w/Value
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_SV)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
rxRule = REGEX_PATTERNS.shortcodeValue
|
||||
hlRule = {
|
||||
1: self._hStyles["code"],
|
||||
2: self._hStyles["value"],
|
||||
3: self._hStyles["code"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
@@ -255,6 +242,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
hlRule = {
|
||||
1: self._hStyles["markup"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
|
||||
# Auto-Replace Tags
|
||||
@@ -263,6 +251,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
hlRule = {
|
||||
0: self._hStyles["replace"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
@@ -280,9 +269,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
def setHandle(self, tHandle: str) -> None:
|
||||
"""Set the handle of the currently highlighted document."""
|
||||
self._tHandle = tHandle
|
||||
self._isInactive = (
|
||||
item.isInactiveClass() if (item := SHARED.project.tree[tHandle]) else False
|
||||
)
|
||||
self._isNovel = False
|
||||
self._isInactive = False
|
||||
if item := SHARED.project.tree[tHandle]:
|
||||
self._isNovel = item.isDocumentLayout()
|
||||
self._isInactive = item.isInactiveClass()
|
||||
logger.debug("Syntax highlighter enabled for item '%s'", tHandle)
|
||||
return
|
||||
|
||||
@@ -397,7 +388,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
elif text.startswith("["): # Special Command
|
||||
self.setCurrentBlockState(BLOCK_TEXT)
|
||||
hRules = self._txtRules
|
||||
hRules = self._txtRules if self._isNovel else self._minRules
|
||||
|
||||
sText = text.rstrip().lower()
|
||||
if sText in ("[newpage]", "[new page]", "[vspace]"):
|
||||
@@ -414,7 +405,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
else: # Text Paragraph
|
||||
self.setCurrentBlockState(BLOCK_TEXT)
|
||||
hRules = self._txtRules
|
||||
hRules = self._txtRules if self._isNovel else self._minRules
|
||||
|
||||
if hRules:
|
||||
for rX, hRule in hRules:
|
||||
|
||||
@@ -165,6 +165,8 @@ class GuiDocViewer(QTextBrowser):
|
||||
self._docTheme.keyword = SHARED.theme.colKey
|
||||
self._docTheme.tag = SHARED.theme.colTag
|
||||
self._docTheme.optional = SHARED.theme.colOpt
|
||||
self._docTheme.dialog = SHARED.theme.colDialN
|
||||
self._docTheme.altdialog = SHARED.theme.colDialA
|
||||
|
||||
# Set default text margins
|
||||
self.document().setDocumentMargin(0)
|
||||
@@ -201,6 +203,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
sPos = self.verticalScrollBar().value()
|
||||
qDoc = ToQTextDocument(SHARED.project)
|
||||
qDoc.setJustify(CONFIG.doJustify)
|
||||
qDoc.setDialogueHighlight(True)
|
||||
qDoc.initDocument(CONFIG.textFont, self._docTheme)
|
||||
qDoc.setKeywords(True)
|
||||
qDoc.setComments(CONFIG.viewComments)
|
||||
@@ -361,8 +364,10 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Process a clicked link in the document."""
|
||||
if link := url.url():
|
||||
logger.debug("Clicked link: '%s'", link)
|
||||
if (bits := link.partition("_")) and bits[2]:
|
||||
if (bits := link.partition("_")) and bits[0] == "#tag" and bits[2]:
|
||||
self.loadDocumentTagRequest.emit(bits[2], nwDocMode.VIEW)
|
||||
else:
|
||||
self.navigateTo(link)
|
||||
return
|
||||
|
||||
@pyqtSlot("QPoint")
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
novelWriter – Text Pattern Functions
|
||||
====================================
|
||||
|
||||
File History:
|
||||
Created: 2024-06-01 [2.5ec1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt5.QtCore import QRegularExpression
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwRegEx
|
||||
from novelwriter.types import QRegExUnicode
|
||||
|
||||
|
||||
class RegExPatterns:
|
||||
|
||||
@property
|
||||
def markdownItalic(self) -> QRegularExpression:
|
||||
"""Markdown italic style."""
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_EI)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
return rxRule
|
||||
|
||||
@property
|
||||
def markdownBold(self) -> QRegularExpression:
|
||||
"""Markdown bold style."""
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_EB)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
return rxRule
|
||||
|
||||
@property
|
||||
def markdownStrike(self) -> QRegularExpression:
|
||||
"""Markdown strikethrough style."""
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_ST)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
return rxRule
|
||||
|
||||
@property
|
||||
def shortcodePlain(self) -> QRegularExpression:
|
||||
"""Plain shortcode style."""
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_SC)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
return rxRule
|
||||
|
||||
@property
|
||||
def shortcodeValue(self) -> QRegularExpression:
|
||||
"""Plain shortcode style."""
|
||||
rxRule = QRegularExpression(nwRegEx.FMT_SV)
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
return rxRule
|
||||
|
||||
@property
|
||||
def dialogStyle(self) -> QRegularExpression:
|
||||
"""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
|
||||
|
||||
rxEnd = "|$" if CONFIG.allowOpenDial else ""
|
||||
rxRule = QRegularExpression(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})")
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
return rxRule
|
||||
|
||||
@property
|
||||
def dialogLine(self) -> QRegularExpression:
|
||||
"""Dialogue line rule based on user settings."""
|
||||
sym = QRegularExpression.escape(CONFIG.dialogLine)
|
||||
rxRule = QRegularExpression(f"^{sym}.*?$")
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
return rxRule
|
||||
|
||||
@property
|
||||
def narratorBreak(self) -> QRegularExpression:
|
||||
"""Dialogue narrator break rule based on user settings."""
|
||||
sym = QRegularExpression.escape(CONFIG.narratorBreak)
|
||||
rxRule = QRegularExpression(f"\\B{sym}\\S.*?\\S{sym}\\B")
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
return rxRule
|
||||
|
||||
@property
|
||||
def altDialogStyle(self) -> QRegularExpression:
|
||||
"""Dialogue alternative rule based on user settings."""
|
||||
symO = QRegularExpression.escape(CONFIG.altDialogOpen)
|
||||
symC = QRegularExpression.escape(CONFIG.altDialogClose)
|
||||
rxRule = QRegularExpression(f"\\B{symO}.*?{symC}\\B")
|
||||
rxRule.setPatternOptions(QRegExUnicode)
|
||||
return rxRule
|
||||
|
||||
|
||||
REGEX_PATTERNS = RegExPatterns()
|
||||
@@ -341,6 +341,8 @@ class GuiManuscript(NToolDialog):
|
||||
theme.keyword = QColor(245, 135, 31)
|
||||
theme.tag = QColor(66, 113, 174)
|
||||
theme.optional = QColor(66, 113, 174)
|
||||
theme.dialog = QColor(66, 113, 174)
|
||||
theme.altdialog = QColor(129, 55, 9)
|
||||
|
||||
self.docPreview.beginNewBuild(len(docBuild))
|
||||
for step, _ in docBuild.iterBuildPreview(theme):
|
||||
|
||||
@@ -1093,11 +1093,13 @@ class _FormatTab(NScrollableForm):
|
||||
self.stripUnicode = NSwitch(self, height=iPx)
|
||||
self.replaceTabs = NSwitch(self, height=iPx)
|
||||
self.keepBreaks = NSwitch(self, height=iPx)
|
||||
self.showDialogue = NSwitch(self, height=iPx)
|
||||
|
||||
self.addRow(self._build.getLabel("format.justifyText"), self.justifyText)
|
||||
self.addRow(self._build.getLabel("format.stripUnicode"), self.stripUnicode)
|
||||
self.addRow(self._build.getLabel("format.replaceTabs"), self.replaceTabs)
|
||||
self.addRow(self._build.getLabel("format.keepBreaks"), self.keepBreaks)
|
||||
self.addRow(self._build.getLabel("format.showDialogue"), self.showDialogue)
|
||||
|
||||
# First Line Indent
|
||||
# =================
|
||||
@@ -1180,6 +1182,7 @@ class _FormatTab(NScrollableForm):
|
||||
self.stripUnicode.setChecked(self._build.getBool("format.stripUnicode"))
|
||||
self.replaceTabs.setChecked(self._build.getBool("format.replaceTabs"))
|
||||
self.keepBreaks.setChecked(self._build.getBool("format.keepBreaks"))
|
||||
self.showDialogue.setChecked(self._build.getBool("format.showDialogue"))
|
||||
|
||||
self.firstIndent.setChecked(self._build.getBool("format.firstLineIndent"))
|
||||
self.indentWidth.setValue(self._build.getFloat("format.firstIndentWidth"))
|
||||
@@ -1219,6 +1222,7 @@ class _FormatTab(NScrollableForm):
|
||||
self._build.setValue("format.stripUnicode", self.stripUnicode.isChecked())
|
||||
self._build.setValue("format.replaceTabs", self.replaceTabs.isChecked())
|
||||
self._build.setValue("format.keepBreaks", self.keepBreaks.isChecked())
|
||||
self._build.setValue("format.showDialogue", self.showDialogue.isChecked())
|
||||
|
||||
self._build.setValue("format.firstLineIndent", self.firstIndent.isChecked())
|
||||
self._build.setValue("format.firstIndentWidth", self.indentWidth.value())
|
||||
|
||||
@@ -19,6 +19,8 @@ mark {background: rgb(255, 255, 166);}
|
||||
.break {text-align: left;}
|
||||
.synopsis {font-style: italic;}
|
||||
.comment {font-style: italic; color: rgb(100, 100, 100);}
|
||||
.dialog {color: rgb(66, 113, 174);}
|
||||
.altdialog {color: rgb(129, 55, 9);}
|
||||
</style>
|
||||
<body>
|
||||
<article>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"meta": {
|
||||
"projectName": "Lorem Ipsum",
|
||||
"novelAuthor": "lipsum.com",
|
||||
"buildTime": 1716803284,
|
||||
"buildTimeStr": "2024-05-27 11:48:04"
|
||||
"buildTime": 1718057434,
|
||||
"buildTimeStr": "2024-06-11 00:10:34"
|
||||
},
|
||||
"text": {
|
||||
"css": [
|
||||
@@ -20,7 +20,9 @@
|
||||
".keyword {color: rgb(245, 135, 31); font-weight: bold;}",
|
||||
".break {text-align: left;}",
|
||||
".synopsis {font-style: italic;}",
|
||||
".comment {font-style: italic; color: rgb(100, 100, 100);}"
|
||||
".comment {font-style: italic; color: rgb(100, 100, 100);}",
|
||||
".dialog {color: rgb(66, 113, 174);}",
|
||||
".altdialog {color: rgb(129, 55, 9);}"
|
||||
],
|
||||
"html": [
|
||||
[
|
||||
|
||||
@@ -24,6 +24,7 @@ import json
|
||||
|
||||
import pytest
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.tohtml import ToHtml
|
||||
|
||||
@@ -260,6 +261,28 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
|
||||
"<a class='tag' href='#tag_Europe'>Europe</a></p>\n"
|
||||
)
|
||||
|
||||
# Dialogue
|
||||
html.setDialogueHighlight(True)
|
||||
html._text = "## Chapter\n\nThis text \u201chas dialogue\u201d in it.\n\n"
|
||||
html.tokenizeText()
|
||||
html.doConvert()
|
||||
assert html.result == (
|
||||
"<h1 style='page-break-before: always;'>Chapter</h1>\n"
|
||||
"<p>This text <span class='dialog'>\u201chas dialogue\u201d</span> in it.</p>\n"
|
||||
)
|
||||
|
||||
# Alt. Dialogue
|
||||
CONFIG.altDialogOpen = "::"
|
||||
CONFIG.altDialogClose = "::"
|
||||
html.setDialogueHighlight(True)
|
||||
html._text = "## Chapter\n\nThis text :: has alt dialogue :: in it.\n\n"
|
||||
html.tokenizeText()
|
||||
html.doConvert()
|
||||
assert html.result == (
|
||||
"<h1 style='page-break-before: always;'>Chapter</h1>\n"
|
||||
"<p>This text <span class='altdialog'>:: has alt dialogue ::</span> in it.</p>\n"
|
||||
)
|
||||
|
||||
# Footnotes
|
||||
# =========
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import pytest
|
||||
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwHeadFmt
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.tokenizer import HeadingFormatter, Tokenizer, stripEscape
|
||||
@@ -1016,88 +1017,151 @@ def testCoreToken_TextFormat(mockGUI):
|
||||
# Text Emphasis
|
||||
tokens._text = "Some **bolded text** on this lines\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"Some bolded text on this lines",
|
||||
[
|
||||
(5, Tokenizer.FMT_B_B, ""),
|
||||
(16, Tokenizer.FMT_B_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
]
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0, "Some bolded text on this lines",
|
||||
[
|
||||
(5, Tokenizer.FMT_B_B, ""),
|
||||
(16, Tokenizer.FMT_B_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
assert tokens.allMarkdown[-1] == "Some **bolded text** on this lines\n\n"
|
||||
|
||||
tokens._text = "Some _italic text_ on this lines\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"Some italic text on this lines",
|
||||
[
|
||||
(5, Tokenizer.FMT_I_B, ""),
|
||||
(16, Tokenizer.FMT_I_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
]
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0, "Some italic text on this lines",
|
||||
[
|
||||
(5, Tokenizer.FMT_I_B, ""),
|
||||
(16, Tokenizer.FMT_I_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
assert tokens.allMarkdown[-1] == "Some _italic text_ on this lines\n\n"
|
||||
|
||||
tokens._text = "Some **_bold italic text_** on this lines\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"Some bold italic text on this lines",
|
||||
[
|
||||
(5, Tokenizer.FMT_B_B, ""),
|
||||
(5, Tokenizer.FMT_I_B, ""),
|
||||
(21, Tokenizer.FMT_I_E, ""),
|
||||
(21, Tokenizer.FMT_B_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
]
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0, "Some bold italic text on this lines",
|
||||
[
|
||||
(5, Tokenizer.FMT_B_B, ""),
|
||||
(5, Tokenizer.FMT_I_B, ""),
|
||||
(21, Tokenizer.FMT_I_E, ""),
|
||||
(21, Tokenizer.FMT_B_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
assert tokens.allMarkdown[-1] == "Some **_bold italic text_** on this lines\n\n"
|
||||
|
||||
tokens._text = "Some ~~strikethrough text~~ on this lines\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"Some strikethrough text on this lines",
|
||||
[
|
||||
(5, Tokenizer.FMT_D_B, ""),
|
||||
(23, Tokenizer.FMT_D_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
]
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0, "Some strikethrough text on this lines",
|
||||
[
|
||||
(5, Tokenizer.FMT_D_B, ""),
|
||||
(23, Tokenizer.FMT_D_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
assert tokens.allMarkdown[-1] == "Some ~~strikethrough text~~ on this lines\n\n"
|
||||
|
||||
tokens._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"Some nested bold and italic and strikethrough text here",
|
||||
[
|
||||
(5, Tokenizer.FMT_B_B, ""),
|
||||
(21, Tokenizer.FMT_I_B, ""),
|
||||
(27, Tokenizer.FMT_I_E, ""),
|
||||
(32, Tokenizer.FMT_D_B, ""),
|
||||
(45, Tokenizer.FMT_D_E, ""),
|
||||
(50, Tokenizer.FMT_B_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
]
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0, "Some nested bold and italic and strikethrough text here",
|
||||
[
|
||||
(5, Tokenizer.FMT_B_B, ""),
|
||||
(21, Tokenizer.FMT_I_B, ""),
|
||||
(27, Tokenizer.FMT_I_E, ""),
|
||||
(32, Tokenizer.FMT_D_B, ""),
|
||||
(45, Tokenizer.FMT_D_E, ""),
|
||||
(50, Tokenizer.FMT_B_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
assert tokens.allMarkdown[-1] == (
|
||||
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToken_Dialogue(mockGUI):
|
||||
"""Test the tokenization of dialogue in the Tokenizer class."""
|
||||
CONFIG.fmtDQuoteOpen = "\u201c"
|
||||
CONFIG.fmtDQuoteClose = "\u201d"
|
||||
CONFIG.fmtSQuoteOpen = "\u2018"
|
||||
CONFIG.fmtSQuoteClose = "\u2019"
|
||||
CONFIG.dialogStyle = 3
|
||||
CONFIG.altDialogOpen = "::"
|
||||
CONFIG.altDialogClose = "::"
|
||||
CONFIG.dialogLine = "\u2013"
|
||||
CONFIG.narratorBreak = "\u2013"
|
||||
|
||||
project = NWProject()
|
||||
tokens = BareTokenizer(project)
|
||||
tokens.setDialogueHighlight(True)
|
||||
|
||||
# Single quotes
|
||||
tokens._text = "Text with \u2018dialogue one,\u2019 and \u2018dialogue two.\u2019\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"Text with \u2018dialogue one,\u2019 and \u2018dialogue two.\u2019",
|
||||
[
|
||||
(10, Tokenizer.FMT_DL_B, ""),
|
||||
(25, Tokenizer.FMT_DL_E, ""),
|
||||
(30, Tokenizer.FMT_DL_B, ""),
|
||||
(45, Tokenizer.FMT_DL_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
|
||||
# Double quotes
|
||||
tokens._text = "Text with \u201cdialogue one,\u201d and \u201cdialogue two.\u201d\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"Text with \u201cdialogue one,\u201d and \u201cdialogue two.\u201d",
|
||||
[
|
||||
(10, Tokenizer.FMT_DL_B, ""),
|
||||
(25, Tokenizer.FMT_DL_E, ""),
|
||||
(30, Tokenizer.FMT_DL_B, ""),
|
||||
(45, Tokenizer.FMT_DL_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
|
||||
# Alt quotes
|
||||
tokens._text = "Text with ::dialogue one,:: and ::dialogue two.::\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"Text with ::dialogue one,:: and ::dialogue two.::",
|
||||
[
|
||||
(10, Tokenizer.FMT_ADL_B, ""),
|
||||
(27, Tokenizer.FMT_ADL_E, ""),
|
||||
(32, Tokenizer.FMT_ADL_B, ""),
|
||||
(49, Tokenizer.FMT_ADL_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
|
||||
# Dialogue line with narrator break
|
||||
tokens._text = "\u2013 Dialogue with a narrator break, \u2013he said,\u2013 see?\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"\u2013 Dialogue with a narrator break, \u2013he said,\u2013 see?",
|
||||
[
|
||||
(0, Tokenizer.FMT_DL_B, ""),
|
||||
(34, Tokenizer.FMT_DL_E, ""),
|
||||
(44, Tokenizer.FMT_DL_B, ""),
|
||||
(49, Tokenizer.FMT_DL_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToken_SpecialFormat(mockGUI):
|
||||
"""Test the tokenization of special formats in the Tokenizer class."""
|
||||
@@ -1267,11 +1331,6 @@ def testCoreToken_ProcessHeaders(mockGUI):
|
||||
project.data.setLanguage("en")
|
||||
project._loadProjectLocalisation()
|
||||
tokens = BareTokenizer(project)
|
||||
|
||||
##
|
||||
# Story Files
|
||||
##
|
||||
|
||||
tokens._isNovel = True
|
||||
|
||||
# Titles
|
||||
|
||||
@@ -234,6 +234,42 @@ def testCoreToOdt_TextFormatting(mockGUI):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToOdt_DialogueFormatting(mockGUI):
|
||||
"""Test formatting of dialogue."""
|
||||
project = NWProject()
|
||||
odt = ToOdt(project, isFlat=True)
|
||||
odt.setDialogueHighlight(True)
|
||||
odt.initDocument()
|
||||
oStyle = ODTParagraphStyle("test")
|
||||
|
||||
# Regular dialogue
|
||||
text = "Text with 'dialogue in it.'"
|
||||
fmt = [(10, odt.FMT_DL_B, ""), (27, odt.FMT_DL_E, "")]
|
||||
xTest = ET.Element(_mkTag("office", "text"))
|
||||
odt._addTextPar(xTest, "Standard", oStyle, text, tFmt=fmt)
|
||||
assert odt.errData == []
|
||||
assert xmlToText(xTest) == (
|
||||
'<office:text>'
|
||||
'<text:p text:style-name="Standard">Text with '
|
||||
'<text:span text:style-name="T1">\'dialogue in it.\'</text:span></text:p>'
|
||||
'</office:text>'
|
||||
)
|
||||
|
||||
# Alternative dialogue
|
||||
text = "Text with ::dialogue in it.::"
|
||||
fmt = [(10, odt.FMT_ADL_B, ""), (29, odt.FMT_ADL_E, "")]
|
||||
xTest = ET.Element(_mkTag("office", "text"))
|
||||
odt._addTextPar(xTest, "Standard", oStyle, text, tFmt=fmt)
|
||||
assert odt.errData == []
|
||||
assert xmlToText(xTest) == (
|
||||
'<office:text>'
|
||||
'<text:p text:style-name="Standard">Text with '
|
||||
'<text:span text:style-name="T2">::dialogue in it.::</text:span></text:p>'
|
||||
'</office:text>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToOdt_ConvertHeaders(mockGUI):
|
||||
"""Test the converter of the ToOdt class."""
|
||||
@@ -854,8 +890,8 @@ def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths):
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToOdt_Format(mockGUI):
|
||||
"""Test the formatters for the ToOdt class."""
|
||||
def testCoreToOdt_SpecialFormats(mockGUI):
|
||||
"""Test the special formatters for the ToOdt class."""
|
||||
project = NWProject()
|
||||
odt = ToOdt(project, isFlat=True)
|
||||
|
||||
@@ -1168,6 +1204,17 @@ def testCoreToOdt_ODTTextStyle():
|
||||
txtStyle.setFontStyle("stuff")
|
||||
assert txtStyle._tAttr["font-style"] == ["fo", None]
|
||||
|
||||
# Text Color
|
||||
assert txtStyle._tAttr["color"] == ["fo", None]
|
||||
txtStyle.setColour("stuff")
|
||||
assert txtStyle._tAttr["color"] == ["fo", None]
|
||||
txtStyle.setColour("012345")
|
||||
assert txtStyle._tAttr["color"] == ["fo", None]
|
||||
txtStyle.setColour("#012345")
|
||||
assert txtStyle._tAttr["color"] == ["fo", "#012345"]
|
||||
txtStyle.setColour("stuff")
|
||||
assert txtStyle._tAttr["color"] == ["fo", None]
|
||||
|
||||
# Background Color
|
||||
assert txtStyle._tAttr["background-color"] == ["fo", None]
|
||||
txtStyle.setBackgroundColour("stuff")
|
||||
|
||||
@@ -44,6 +44,8 @@ THEME.modifier = QColor(129, 55, 9)
|
||||
THEME.keyword = QColor(245, 135, 31)
|
||||
THEME.tag = QColor(66, 113, 174)
|
||||
THEME.optional = QColor(66, 113, 174)
|
||||
THEME.dialog = QColor(113, 140, 0)
|
||||
THEME.altdialog = QColor(234, 183, 0)
|
||||
|
||||
|
||||
def charFmtInBlock(block: QTextBlock, pos: int) -> QTextCharFormat:
|
||||
@@ -441,11 +443,17 @@ def testCoreToQTextDocument_TextBlockFormats(mockGUI):
|
||||
@pytest.mark.core
|
||||
def testCoreToQTextDocument_TextCharFormats(mockGUI):
|
||||
"""Test text char formats in the ToQTextDocument class."""
|
||||
CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO
|
||||
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
|
||||
CONFIG.altDialogOpen = "|<"
|
||||
CONFIG.altDialogClose = ">|"
|
||||
|
||||
project = NWProject()
|
||||
qdoc = ToQTextDocument(project)
|
||||
|
||||
# Convert before init
|
||||
qdoc._text = "Blabla"
|
||||
qdoc.setDialogueHighlight(True)
|
||||
qdoc.doConvert()
|
||||
qdoc.tokenizeText()
|
||||
assert qdoc.document.toPlainText() == ""
|
||||
@@ -465,10 +473,12 @@ def testCoreToQTextDocument_TextCharFormats(mockGUI):
|
||||
"With [m]highlighted[/m] text\n\n"
|
||||
"With super[sup]script[/sup] text\n\n"
|
||||
"With sub[sub]script[/sub] text\n\n"
|
||||
"With \u201cdialog\u201d text\n\n"
|
||||
"With |<alternative dialog>| text\n\n"
|
||||
)
|
||||
qdoc.tokenizeText()
|
||||
qdoc.doConvert()
|
||||
assert qdoc.document.blockCount() == 8
|
||||
assert qdoc.document.blockCount() == 10
|
||||
|
||||
# 0: Scene
|
||||
block = qdoc.document.findBlockByNumber(0)
|
||||
@@ -544,6 +554,26 @@ def testCoreToQTextDocument_TextCharFormats(mockGUI):
|
||||
cFmt = charFmtInBlock(block, 15)
|
||||
assert cFmt.verticalAlignment() == QtVAlignNormal
|
||||
|
||||
# 8: Dialogue
|
||||
block = qdoc.document.findBlockByNumber(8)
|
||||
assert block.text() == "With \u201cdialog\u201d text"
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground() == THEME.text
|
||||
cFmt = charFmtInBlock(block, 6)
|
||||
assert cFmt.foreground() == THEME.dialog
|
||||
cFmt = charFmtInBlock(block, 14)
|
||||
assert cFmt.foreground() == THEME.text
|
||||
|
||||
# 9: Alt. Dialogue
|
||||
block = qdoc.document.findBlockByNumber(9)
|
||||
assert block.text() == "With |<alternative dialog>| text"
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground() == THEME.text
|
||||
cFmt = charFmtInBlock(block, 6)
|
||||
assert cFmt.foreground() == THEME.altdialog
|
||||
cFmt = charFmtInBlock(block, 28)
|
||||
assert cFmt.foreground() == THEME.text
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToQTextDocument_Footnotes(mockGUI):
|
||||
|
||||
@@ -160,6 +160,11 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
docViewer._linkClicked(QUrl("#tag_bod"))
|
||||
assert docViewer.docHandle == "4c4f28287af27"
|
||||
|
||||
# Other links should just trigger a navigate call
|
||||
with qtbot.waitSignal(docViewer.sourceChanged, timeout=1000) as signal:
|
||||
docViewer._linkClicked(QUrl("#somewhere_else"))
|
||||
assert signal.args[0].url() == "#somewhere_else"
|
||||
|
||||
# Click mouse nav buttons
|
||||
qtbot.mouseClick(docViewer.viewport(), Qt.BackButton, pos=rect.center(), delay=100)
|
||||
assert docViewer.docHandle == "88243afbe5ed8"
|
||||
|
||||
@@ -64,9 +64,6 @@ def testGuiI18n_Localisation(qtbot, monkeypatch, language, nwGUI, projPath):
|
||||
qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(dType) is not None, timeout=1000)
|
||||
dialog = SHARED.findTopLevelWidget(dType)
|
||||
assert isinstance(dialog, dType)
|
||||
with qtbot.waitExposed(dialog):
|
||||
dialog.show()
|
||||
dialog.close()
|
||||
|
||||
showDialog(nwGUI.showWelcomeDialog, GuiWelcome)
|
||||
showDialog(nwGUI.showPreferencesDialog, GuiPreferences)
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
novelWriter – Patterns Module Tester
|
||||
====================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtCore import QRegularExpression
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwUnicode
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS
|
||||
|
||||
|
||||
def allMatches(regEx: QRegularExpression, text: str) -> list[list[str]]:
|
||||
"""Get all matches for a regex."""
|
||||
result = []
|
||||
itt = regEx.globalMatch(text, 0)
|
||||
while itt.hasNext():
|
||||
match = itt.next()
|
||||
result.append([
|
||||
(match.captured(n), match.capturedStart(n), match.capturedEnd(n))
|
||||
for n in range(match.lastCapturedIndex() + 1)
|
||||
])
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_Markdown():
|
||||
"""Test the markdown pattern regexes."""
|
||||
# Bold
|
||||
regEx = REGEX_PATTERNS.markdownBold
|
||||
assert allMatches(regEx, "one **two** three") == [
|
||||
[("**two**", 4, 11), ("**", 4, 6), ("two", 6, 9), ("**", 9, 11)]
|
||||
]
|
||||
assert allMatches(regEx, "one **two* three") == []
|
||||
assert allMatches(regEx, "one *two** three") == []
|
||||
assert allMatches(regEx, "one**two**three") == []
|
||||
|
||||
# Italic
|
||||
regEx = REGEX_PATTERNS.markdownItalic
|
||||
assert allMatches(regEx, "one _two_ three") == [
|
||||
[("_two_", 4, 9), ("_", 4, 5), ("two", 5, 8), ("_", 8, 9)]
|
||||
]
|
||||
assert allMatches(regEx, "one __two_ three") == []
|
||||
assert allMatches(regEx, "one _two__ three") == [
|
||||
[("_two__", 4, 10), ("_", 4, 5), ("two_", 5, 9), ("_", 9, 10)]
|
||||
]
|
||||
assert allMatches(regEx, "one_two_three") == []
|
||||
|
||||
# Strike
|
||||
regEx = REGEX_PATTERNS.markdownStrike
|
||||
assert allMatches(regEx, "one ~~two~~ three") == [
|
||||
[("~~two~~", 4, 11), ("~~", 4, 6), ("two", 6, 9), ("~~", 9, 11)]
|
||||
]
|
||||
assert allMatches(regEx, "one ~~two~ three") == []
|
||||
assert allMatches(regEx, "one ~two~~ three") == []
|
||||
assert allMatches(regEx, "one~~two~~three") == []
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_ShortcodesPlain():
|
||||
"""Test the shortcode pattern regexes."""
|
||||
regEx = REGEX_PATTERNS.shortcodePlain
|
||||
|
||||
# Test Usage
|
||||
# ==========
|
||||
|
||||
# General, normal usage
|
||||
assert allMatches(regEx, "one [b]two[/b] three") == [
|
||||
[("[b]", 4, 7), ("[b]", 4, 7)],
|
||||
[("[/b]", 10, 14), ("[/b]", 10, 14)],
|
||||
]
|
||||
|
||||
# General, no spaces
|
||||
assert allMatches(regEx, "one[b]two[/b]three") == [
|
||||
[("[b]", 3, 6), ("[b]", 3, 6)],
|
||||
[("[/b]", 9, 13), ("[/b]", 9, 13)],
|
||||
]
|
||||
|
||||
# General, with padding
|
||||
assert allMatches(regEx, "one [b] two [/b] three") == [
|
||||
[("[b]", 4, 7), ("[b]", 4, 7)],
|
||||
[("[/b]", 12, 16), ("[/b]", 12, 16)],
|
||||
]
|
||||
|
||||
# General, with escapes
|
||||
assert allMatches(regEx, "one \\[b]two[/b\\] three") == []
|
||||
|
||||
# Test Formats
|
||||
# ============
|
||||
|
||||
# Bold
|
||||
assert allMatches(regEx, "one [b]two[/b] three") == [
|
||||
[("[b]", 4, 7), ("[b]", 4, 7)],
|
||||
[("[/b]", 10, 14), ("[/b]", 10, 14)],
|
||||
]
|
||||
|
||||
# Italic
|
||||
assert allMatches(regEx, "one [i]two[/i] three") == [
|
||||
[("[i]", 4, 7), ("[i]", 4, 7)],
|
||||
[("[/i]", 10, 14), ("[/i]", 10, 14)],
|
||||
]
|
||||
|
||||
# Strike
|
||||
assert allMatches(regEx, "one [s]two[/s] three") == [
|
||||
[("[s]", 4, 7), ("[s]", 4, 7)],
|
||||
[("[/s]", 10, 14), ("[/s]", 10, 14)],
|
||||
]
|
||||
|
||||
# Underline
|
||||
assert allMatches(regEx, "one [u]two[/u] three") == [
|
||||
[("[u]", 4, 7), ("[u]", 4, 7)],
|
||||
[("[/u]", 10, 14), ("[/u]", 10, 14)],
|
||||
]
|
||||
|
||||
# Mark
|
||||
assert allMatches(regEx, "one [m]two[/m] three") == [
|
||||
[("[m]", 4, 7), ("[m]", 4, 7)],
|
||||
[("[/m]", 10, 14), ("[/m]", 10, 14)],
|
||||
]
|
||||
|
||||
# Superscript
|
||||
assert allMatches(regEx, "one [sup]two[/sup] three") == [
|
||||
[("[sup]", 4, 9), ("[sup]", 4, 9)],
|
||||
[("[/sup]", 12, 18), ("[/sup]", 12, 18)],
|
||||
]
|
||||
|
||||
# Subscript
|
||||
assert allMatches(regEx, "one [sub]two[/sub] three") == [
|
||||
[("[sub]", 4, 9), ("[sub]", 4, 9)],
|
||||
[("[/sub]", 12, 18), ("[/sub]", 12, 18)],
|
||||
]
|
||||
|
||||
# Test Invalid
|
||||
# ============
|
||||
|
||||
assert allMatches(regEx, "one [x]two[/x] three") == []
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_ShortcodesValue():
|
||||
"""Test the shortcode with value pattern regexes."""
|
||||
regEx = REGEX_PATTERNS.shortcodeValue
|
||||
|
||||
assert allMatches(regEx, "one [footnote:two] three") == [
|
||||
[("[footnote:two]", 4, 18), ("[footnote:", 4, 14), ("two", 14, 17), ("]", 17, 18)]
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextPatterns_DialogueStyle():
|
||||
"""Test the 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
|
||||
|
||||
CONFIG.dialogStyle = 3
|
||||
|
||||
# Closed
|
||||
# ======
|
||||
|
||||
CONFIG.allowOpenDial = False
|
||||
regEx = REGEX_PATTERNS.dialogStyle
|
||||
|
||||
# Defined single quotes are recognised
|
||||
assert allMatches(regEx, "one \u2018two\u2019 three") == [
|
||||
[("\u2018two\u2019", 4, 9)]
|
||||
]
|
||||
|
||||
# Defined double quotes are recognised
|
||||
assert allMatches(regEx, "one \u201ctwo\u201d three") == [
|
||||
[("\u201ctwo\u201d", 4, 9)]
|
||||
]
|
||||
|
||||
# Straight single quotes are ignored
|
||||
assert allMatches(regEx, "one 'two' three") == []
|
||||
|
||||
# Straight double quotes are ignored
|
||||
assert allMatches(regEx, "one \"two\" three") == []
|
||||
|
||||
# Skipping whitespace is not allowed
|
||||
assert allMatches(regEx, "one\u2018two\u2019three") == []
|
||||
|
||||
# Open
|
||||
# ====
|
||||
|
||||
CONFIG.allowOpenDial = True
|
||||
regEx = REGEX_PATTERNS.dialogStyle
|
||||
|
||||
# Defined single quotes are recognised also when open
|
||||
assert allMatches(regEx, "one \u2018two three") == [
|
||||
[("\u2018two three", 4, 14)]
|
||||
]
|
||||
|
||||
# Defined double quotes are recognised also when open
|
||||
assert allMatches(regEx, "one \u201ctwo three") == [
|
||||
[("\u201ctwo three", 4, 14)]
|
||||
]
|
||||
|
||||
|
||||
@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
|
||||
|
||||
CONFIG.dialogStyle = 3
|
||||
CONFIG.dialogLine = nwUnicode.U_ENDASH
|
||||
CONFIG.narratorBreak = nwUnicode.U_ENDASH
|
||||
CONFIG.altDialogOpen = "::"
|
||||
CONFIG.altDialogClose = "::"
|
||||
|
||||
# Dialogue Line
|
||||
# =============
|
||||
regEx = REGEX_PATTERNS.dialogLine
|
||||
|
||||
# Check dialogue line in first position
|
||||
assert allMatches(regEx, "\u2013 one two three") == [
|
||||
[("\u2013 one two three", 0, 15)]
|
||||
]
|
||||
|
||||
# Check dialogue line in second position
|
||||
assert allMatches(regEx, " \u2013 one two three") == []
|
||||
|
||||
# Narrator Break
|
||||
# ==============
|
||||
regEx = REGEX_PATTERNS.narratorBreak
|
||||
|
||||
# Narrator break with no padding
|
||||
assert allMatches(regEx, "one \u2013two\u2013 three") == [
|
||||
[("\u2013two\u2013", 4, 9)]
|
||||
]
|
||||
|
||||
# Narrator break with padding
|
||||
assert allMatches(regEx, "one \u2013 two \u2013 three") == []
|
||||
|
||||
# Alternative Dialogue
|
||||
# ====================
|
||||
regEx = REGEX_PATTERNS.altDialogStyle
|
||||
|
||||
# With no padding
|
||||
assert allMatches(regEx, "one ::two:: three") == [
|
||||
[("::two::", 4, 11)]
|
||||
]
|
||||
|
||||
# With padding
|
||||
assert allMatches(regEx, "one :: two :: three") == [
|
||||
[(":: two ::", 4, 13)]
|
||||
]
|
||||
@@ -555,6 +555,12 @@ def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
|
||||
build.setValue("format.justifyText", False)
|
||||
build.setValue("format.stripUnicode", False)
|
||||
build.setValue("format.replaceTabs", False)
|
||||
build.setValue("format.keepBreaks", True)
|
||||
build.setValue("format.showDialogue", False)
|
||||
|
||||
build.setValue("format.firstLineIndent", False)
|
||||
build.setValue("format.firstIndentWidth", 1.4)
|
||||
build.setValue("format.indentFirstPar", False)
|
||||
|
||||
build.setValue("format.pageUnit", "mm")
|
||||
build.setValue("format.pageSize", "Custom")
|
||||
@@ -580,6 +586,8 @@ def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
|
||||
assert fmtTab.justifyText.isChecked() is False
|
||||
assert fmtTab.stripUnicode.isChecked() is False
|
||||
assert fmtTab.replaceTabs.isChecked() is False
|
||||
assert fmtTab.keepBreaks.isChecked() is True
|
||||
assert fmtTab.showDialogue.isChecked() is False
|
||||
|
||||
assert fmtTab.firstIndent.isChecked() is False
|
||||
assert fmtTab.indentWidth.value() == 1.4
|
||||
@@ -603,6 +611,8 @@ def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
|
||||
fmtTab.justifyText.setChecked(True)
|
||||
fmtTab.stripUnicode.setChecked(True)
|
||||
fmtTab.replaceTabs.setChecked(True)
|
||||
fmtTab.keepBreaks.setChecked(False)
|
||||
fmtTab.showDialogue.setChecked(True)
|
||||
|
||||
fmtTab.firstIndent.setChecked(True)
|
||||
fmtTab.indentWidth.setValue(2.0)
|
||||
@@ -620,6 +630,8 @@ def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
|
||||
assert build.getBool("format.justifyText") is True
|
||||
assert build.getBool("format.stripUnicode") is True
|
||||
assert build.getBool("format.replaceTabs") is True
|
||||
assert build.getBool("format.keepBreaks") is False
|
||||
assert build.getBool("format.showDialogue") is True
|
||||
|
||||
assert build.getBool("format.firstLineIndent") is True
|
||||
assert build.getFloat("format.firstIndentWidth") == 2.0
|
||||
|
||||
Reference in New Issue
Block a user