Extend dialogue highlighting (#1908)

This commit is contained in:
Veronica Berglyd Olsen
2024-06-11 00:16:32 +02:00
committed by GitHub
23 changed files with 810 additions and 154 deletions
+4 -4
View File
@@ -61,10 +61,10 @@ class nwConst:
class nwRegEx: class nwRegEx:
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = 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_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])"
FMT_SV = r"(?<!\\)(\[(?i)(?:footnote):)(.+?)(?<!\\)(\])" FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
class nwShortcode: class nwShortcode:
+2
View File
@@ -82,6 +82,7 @@ SETTINGS_TEMPLATE = {
"format.stripUnicode": (bool, False), "format.stripUnicode": (bool, False),
"format.replaceTabs": (bool, False), "format.replaceTabs": (bool, False),
"format.keepBreaks": (bool, True), "format.keepBreaks": (bool, True),
"format.showDialogue": (bool, False),
"format.firstLineIndent": (bool, False), "format.firstLineIndent": (bool, False),
"format.firstIndentWidth": (float, 1.4), "format.firstIndentWidth": (float, 1.4),
"format.indentFirstPar": (bool, False), "format.indentFirstPar": (bool, False),
@@ -131,6 +132,7 @@ SETTINGS_LABELS = {
"format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"), "format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"),
"format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"), "format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"),
"format.keepBreaks": QT_TRANSLATE_NOOP("Builds", "Preserve Hard Line Breaks"), "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.grpParIndent": QT_TRANSLATE_NOOP("Builds", "First Line Indent"),
"format.firstLineIndent": QT_TRANSLATE_NOOP("Builds", "Enable Indent"), "format.firstLineIndent": QT_TRANSLATE_NOOP("Builds", "Enable Indent"),
+1
View File
@@ -338,6 +338,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.setFirstLineIndent( bldObj.setFirstLineIndent(
self._build.getBool("format.firstLineIndent"), self._build.getBool("format.firstLineIndent"),
self._build.getFloat("format.firstIndentWidth"), self._build.getFloat("format.firstIndentWidth"),
+7 -1
View File
@@ -52,6 +52,10 @@ HTML5_TAGS = {
Tokenizer.FMT_SUP_E: "</sup>", Tokenizer.FMT_SUP_E: "</sup>",
Tokenizer.FMT_SUB_B: "<sub>", Tokenizer.FMT_SUB_B: "<sub>",
Tokenizer.FMT_SUB_E: "</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: "", Tokenizer.FMT_STRIP: "",
} }
@@ -431,6 +435,8 @@ class ToHtml(Tokenizer):
styles.append(".break {text-align: left;}") styles.append(".break {text-align: left;}")
styles.append(".synopsis {font-style: italic;}") styles.append(".synopsis {font-style: italic;}")
styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}") 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 return styles
@@ -451,7 +457,7 @@ class ToHtml(Tokenizer):
else: else:
html = "<sup>ERR</sup>" html = "<sup>ERR</sup>"
else: else:
html = HTML5_TAGS.get(fmt, "ERR") html = HTML5_TAGS.get(fmt, "")
temp = f"{temp[:pos]}{html}{temp[pos:]}" temp = f"{temp[:pos]}{html}{temp[pos:]}"
temp = temp.replace("\n", "<br>") temp = temp.replace("\n", "<br>")
return stripEscape(temp) return stripEscape(temp)
+50 -11
View File
@@ -36,13 +36,13 @@ from time import time
from PyQt5.QtCore import QCoreApplication, QRegularExpression from PyQt5.QtCore import QCoreApplication, QRegularExpression
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from novelwriter import CONFIG
from novelwriter.common import checkInt, formatTimeStamp, numberToRoman from novelwriter.common import checkInt, formatTimeStamp, numberToRoman
from novelwriter.constants import ( from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst
nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst
)
from novelwriter.core.index import processComment from novelwriter.core.index import processComment
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.enum import nwComment, nwItemLayout from novelwriter.enum import nwComment, nwItemLayout
from novelwriter.text.patterns import REGEX_PATTERNS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -85,8 +85,12 @@ class Tokenizer(ABC):
FMT_SUP_E = 12 # End superscript FMT_SUP_E = 12 # End superscript
FMT_SUB_B = 13 # Begin subscript FMT_SUB_B = 13 # Begin subscript
FMT_SUB_E = 14 # End subscript FMT_SUB_E = 14 # End subscript
FMT_FNOTE = 15 # Footnote marker FMT_DL_B = 15 # Begin dialogue
FMT_STRIP = 16 # Strip the format code 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 # Block Type
T_EMPTY = 1 # Empty line (new paragraph) T_EMPTY = 1 # Empty line (new paragraph)
@@ -193,7 +197,8 @@ class Tokenizer(ABC):
# Instance Variables # Instance Variables
self._hFormatter = HeadingFormatter(self._project) 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 # This File
self._isNovel = False # Document is a novel document self._isNovel = False # Document is a novel document
@@ -208,12 +213,12 @@ class Tokenizer(ABC):
# Format RegEx # Format RegEx
self._rxMarkdown = [ self._rxMarkdown = [
(QRegularExpression(nwRegEx.FMT_EI), [0, self.FMT_I_B, 0, self.FMT_I_E]), (REGEX_PATTERNS.markdownItalic, [0, self.FMT_I_B, 0, self.FMT_I_E]),
(QRegularExpression(nwRegEx.FMT_EB), [0, self.FMT_B_B, 0, self.FMT_B_E]), (REGEX_PATTERNS.markdownBold, [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.markdownStrike, [0, self.FMT_D_B, 0, self.FMT_D_E]),
] ]
self._rxShortCodes = QRegularExpression(nwRegEx.FMT_SC) self._rxShortCodes = REGEX_PATTERNS.shortcodePlain
self._rxShortCodeVals = QRegularExpression(nwRegEx.FMT_SV) self._rxShortCodeVals = REGEX_PATTERNS.shortcodeValue
self._shortCodeFmt = { self._shortCodeFmt = {
nwShortcode.ITALIC_O: self.FMT_I_B, nwShortcode.ITALIC_C: self.FMT_I_E, 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, nwShortcode.FOOTNOTE_B: self.FMT_FNOTE,
} }
self._rxDialogue: list[tuple[QRegularExpression, int, int]] = []
return return
## ##
@@ -349,6 +356,29 @@ class Tokenizer(ABC):
self._doJustify = state self._doJustify = state
return 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: def setTitleMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower title margin.""" """Set the upper and lower title margin."""
self._marginTitle = (float(upper), float(lower)) self._marginTitle = (float(upper), float(lower))
@@ -1106,6 +1136,15 @@ class Tokenizer(ABC):
f"{tHandle}:{rxMatch.captured(2)}", 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 # Post-process text and format
result = text result = text
formats = [] formats = []
+45 -14
View File
@@ -82,13 +82,15 @@ TAG_SPAN = _mkTag("text", "span")
TAG_STNM = _mkTag("text", "style-name") TAG_STNM = _mkTag("text", "style-name")
# Formatting Codes # Formatting Codes
X_BLD = 0x01 # Bold format X_BLD = 0x001 # Bold format
X_ITA = 0x02 # Italic format X_ITA = 0x002 # Italic format
X_DEL = 0x04 # Strikethrough format X_DEL = 0x004 # Strikethrough format
X_UND = 0x08 # Underline format X_UND = 0x008 # Underline format
X_MRK = 0x10 # Marked format X_MRK = 0x010 # Marked format
X_SUP = 0x20 # Superscript X_SUP = 0x020 # Superscript
X_SUB = 0x40 # Subscript X_SUB = 0x040 # Subscript
X_DLG = 0x080 # Dialogue
X_DLA = 0x100 # Alt. Dialogue
# Formatting Masks # Formatting Masks
M_BLD = ~X_BLD M_BLD = ~X_BLD
@@ -98,6 +100,8 @@ M_UND = ~X_UND
M_MRK = ~X_MRK M_MRK = ~X_MRK
M_SUP = ~X_SUP M_SUP = ~X_SUP
M_SUB = ~X_SUB M_SUB = ~X_SUB
M_DLG = ~X_DLG
M_DLA = ~X_DLA
# ODT Styles # ODT Styles
S_TITLE = "Title" S_TITLE = "Title"
@@ -216,13 +220,15 @@ class ToOdt(Tokenizer):
self._mDocRight = "2.000cm" self._mDocRight = "2.000cm"
# Colour # Colour
self._colHead12 = None self._colHead12 = None
self._opaHead12 = None self._opaHead12 = None
self._colHead34 = None self._colHead34 = None
self._opaHead34 = None self._opaHead34 = None
self._colMetaTx = None self._colMetaTx = None
self._opaMetaTx = None self._opaMetaTx = None
self._markText = "#ffffa6" self._colDialogM = None
self._colDialogA = None
self._markText = "#ffffa6"
return return
@@ -324,6 +330,10 @@ class ToOdt(Tokenizer):
self._colMetaTx = "#813709" self._colMetaTx = "#813709"
self._opaMetaTx = "100%" self._opaMetaTx = "100%"
if self._showDialog:
self._colDialogM = "#2a6099"
self._colDialogA = "#813709"
self._fLineHeight = f"{round(100 * self._lineHeight):d}%" self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
self._fBlockIndent = self._emToCm(self._blockIndent) self._fBlockIndent = self._emToCm(self._blockIndent)
self._fTextIndent = self._emToCm(self._firstWidth) self._fTextIndent = self._emToCm(self._firstWidth)
@@ -684,6 +694,14 @@ class ToOdt(Tokenizer):
xFmt |= X_SUB xFmt |= X_SUB
elif fFmt == self.FMT_SUB_E: elif fFmt == self.FMT_SUB_E:
xFmt &= M_SUB 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: elif fFmt == self.FMT_FNOTE:
xNode = self._generateFootnote(fData) xNode = self._generateFootnote(fData)
elif fFmt == self.FMT_STRIP: elif fFmt == self.FMT_STRIP:
@@ -757,6 +775,10 @@ class ToOdt(Tokenizer):
style.setTextPosition("super") style.setTextPosition("super")
if hFmt & X_SUB: if hFmt & X_SUB:
style.setTextPosition("sub") style.setTextPosition("sub")
if hFmt & X_DLG:
style.setColour(self._colDialogM)
if hFmt & X_DLA:
style.setColour(self._colDialogA)
self._autoText[hFmt] = style self._autoText[hFmt] = style
return style.name return style.name
@@ -1357,6 +1379,7 @@ class ODTTextStyle:
self._tAttr = { self._tAttr = {
"font-weight": ["fo", None], "font-weight": ["fo", None],
"font-style": ["fo", None], "font-style": ["fo", None],
"color": ["fo", None],
"background-color": ["fo", None], "background-color": ["fo", None],
"text-position": ["style", None], "text-position": ["style", None],
"text-line-through-style": ["style", None], "text-line-through-style": ["style", None],
@@ -1391,6 +1414,14 @@ class ODTTextStyle:
self._tAttr["font-style"][1] = None self._tAttr["font-style"][1] = None
return 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: def setBackgroundColour(self, value: str | None) -> None:
"""Set text background colour.""" """Set text background colour."""
if value and len(value) == 7 and value[0] == "#": if value and len(value) == 7 and value[0] == "#":
+19 -9
View File
@@ -45,16 +45,18 @@ T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
class TextDocumentTheme: class TextDocumentTheme:
text: QColor = QtBlack text: QColor = QtBlack
highlight: QColor = QtTransparent highlight: QColor = QtTransparent
head: QColor = QtBlack head: QColor = QtBlack
comment: QColor = QtBlack comment: QColor = QtBlack
note: QColor = QtBlack note: QColor = QtBlack
code: QColor = QtBlack code: QColor = QtBlack
modifier: QColor = QtBlack modifier: QColor = QtBlack
keyword: QColor = QtBlack keyword: QColor = QtBlack
tag: QColor = QtBlack tag: QColor = QtBlack
optional: QColor = QtBlack optional: QColor = QtBlack
dialog: QColor = QtBlack
altdialog: QColor = QtBlack
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None: def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
@@ -340,6 +342,14 @@ class ToQTextDocument(Tokenizer):
cFmt.setVerticalAlignment(QtVAlignSub) cFmt.setVerticalAlignment(QtVAlignSub)
elif fmt == self.FMT_SUB_E: elif fmt == self.FMT_SUB_E:
cFmt.setVerticalAlignment(QtVAlignNormal) 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: elif fmt == self.FMT_FNOTE:
xFmt = QTextCharFormat(self._cCode) xFmt = QTextCharFormat(self._cCode)
xFmt.setVerticalAlignment(QtVAlignSuper) xFmt.setVerticalAlignment(QtVAlignSuper)
+3
View File
@@ -2030,6 +2030,9 @@ class GuiDocEditor(QPlainTextEdit):
cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete) cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete)
cursor.insertText(tInsert) cursor.insertText(tInsert)
# Re-highlight, since the auto-replace sometimes interferes with it
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
return return
@staticmethod @staticmethod
+30 -39
View File
@@ -39,6 +39,7 @@ from novelwriter.common import checkInt
from novelwriter.constants import nwHeaders, nwRegEx, nwUnicode from novelwriter.constants import nwHeaders, nwRegEx, 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.types import QRegExUnicode from novelwriter.types import QRegExUnicode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -59,8 +60,8 @@ BLOCK_TITLE = 4
class GuiDocHighlighter(QSyntaxHighlighter): class GuiDocHighlighter(QSyntaxHighlighter):
__slots__ = ( __slots__ = (
"_tHandle", "_isInactive", "_spellCheck", "_spellErr", "_hStyles", "_tHandle", "_isNovel", "_isInactive", "_spellCheck", "_spellErr",
"_txtRules", "_cmnRules", "_hStyles", "_minRules", "_txtRules", "_cmnRules",
) )
def __init__(self, document: QTextDocument) -> None: def __init__(self, document: QTextDocument) -> None:
@@ -69,11 +70,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
logger.debug("Create: GuiDocHighlighter") logger.debug("Create: GuiDocHighlighter")
self._tHandle = None self._tHandle = None
self._isNovel = False
self._isInactive = False self._isInactive = False
self._spellCheck = False self._spellCheck = False
self._spellErr = QTextCharFormat() self._spellErr = QTextCharFormat()
self._hStyles: dict[str, QTextCharFormat] = {} self._hStyles: dict[str, QTextCharFormat] = {}
self._minRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
self._txtRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = [] self._txtRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
self._cmnRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = [] self._cmnRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
@@ -137,6 +140,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
hlRule = { hlRule = {
0: self._hStyles["mspaces"], 0: self._hStyles["mspaces"],
} }
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule))
@@ -146,106 +150,89 @@ class GuiDocHighlighter(QSyntaxHighlighter):
hlRule = { hlRule = {
0: self._hStyles["nobreak"], 0: self._hStyles["nobreak"],
} }
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule))
# Dialogue # Dialogue
if CONFIG.dialogStyle > 0: if CONFIG.dialogStyle > 0:
symO = "" rxRule = REGEX_PATTERNS.dialogStyle
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)
hlRule = { hlRule = {
0: self._hStyles["dialog"], 0: self._hStyles["dialog"],
} }
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
if CONFIG.dialogLine: if CONFIG.dialogLine:
sym = QRegularExpression.escape(CONFIG.dialogLine) rxRule = REGEX_PATTERNS.dialogLine
rxRule = QRegularExpression(f"^{sym}.*?$")
rxRule.setPatternOptions(QRegExUnicode)
hlRule = { hlRule = {
0: self._hStyles["dialog"], 0: self._hStyles["dialog"],
} }
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
if CONFIG.narratorBreak: if CONFIG.narratorBreak:
sym = QRegularExpression.escape(CONFIG.narratorBreak) rxRule = REGEX_PATTERNS.narratorBreak
rxRule = QRegularExpression(f"({sym}\\b)(.*?)(\\b{sym})")
rxRule.setPatternOptions(QRegExUnicode)
hlRule = { hlRule = {
0: self._hStyles["text"], 0: self._hStyles["text"],
} }
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
if CONFIG.altDialogOpen and CONFIG.altDialogClose: if CONFIG.altDialogOpen and CONFIG.altDialogClose:
symO = QRegularExpression.escape(CONFIG.altDialogOpen) rxRule = REGEX_PATTERNS.altDialogStyle
symC = QRegularExpression.escape(CONFIG.altDialogClose)
rxRule = QRegularExpression(f"\\B{symO}.*?{symC}\\B")
rxRule.setPatternOptions(QRegExUnicode)
hlRule = { hlRule = {
0: self._hStyles["altdialog"], 0: self._hStyles["altdialog"],
} }
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
# Markdown Italic # Markdown Italic
rxRule = QRegularExpression(nwRegEx.FMT_EI) rxRule = REGEX_PATTERNS.markdownItalic
rxRule.setPatternOptions(QRegExUnicode)
hlRule = { hlRule = {
1: self._hStyles["markup"], 1: self._hStyles["markup"],
2: self._hStyles["italic"], 2: self._hStyles["italic"],
3: self._hStyles["markup"], 3: self._hStyles["markup"],
} }
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule))
# Markdown Bold # Markdown Bold
rxRule = QRegularExpression(nwRegEx.FMT_EB) rxRule = REGEX_PATTERNS.markdownBold
rxRule.setPatternOptions(QRegExUnicode)
hlRule = { hlRule = {
1: self._hStyles["markup"], 1: self._hStyles["markup"],
2: self._hStyles["bold"], 2: self._hStyles["bold"],
3: self._hStyles["markup"], 3: self._hStyles["markup"],
} }
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule))
# Markdown Strikethrough # Markdown Strikethrough
rxRule = QRegularExpression(nwRegEx.FMT_ST) rxRule = REGEX_PATTERNS.markdownStrike
rxRule.setPatternOptions(QRegExUnicode)
hlRule = { hlRule = {
1: self._hStyles["markup"], 1: self._hStyles["markup"],
2: self._hStyles["strike"], 2: self._hStyles["strike"],
3: self._hStyles["markup"], 3: self._hStyles["markup"],
} }
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule))
# Shortcodes # Shortcodes
rxRule = QRegularExpression(nwRegEx.FMT_SC) rxRule = REGEX_PATTERNS.shortcodePlain
rxRule.setPatternOptions(QRegExUnicode)
hlRule = { hlRule = {
1: self._hStyles["code"], 1: self._hStyles["code"],
} }
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule))
# Shortcodes w/Value # Shortcodes w/Value
rxRule = QRegularExpression(nwRegEx.FMT_SV) rxRule = REGEX_PATTERNS.shortcodeValue
rxRule.setPatternOptions(QRegExUnicode)
hlRule = { hlRule = {
1: self._hStyles["code"], 1: self._hStyles["code"],
2: self._hStyles["value"], 2: self._hStyles["value"],
3: self._hStyles["code"], 3: self._hStyles["code"],
} }
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule))
@@ -255,6 +242,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
hlRule = { hlRule = {
1: self._hStyles["markup"], 1: self._hStyles["markup"],
} }
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
# Auto-Replace Tags # Auto-Replace Tags
@@ -263,6 +251,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
hlRule = { hlRule = {
0: self._hStyles["replace"], 0: self._hStyles["replace"],
} }
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule)) self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule))
@@ -280,9 +269,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
def setHandle(self, tHandle: str) -> None: def setHandle(self, tHandle: str) -> None:
"""Set the handle of the currently highlighted document.""" """Set the handle of the currently highlighted document."""
self._tHandle = tHandle self._tHandle = tHandle
self._isInactive = ( self._isNovel = False
item.isInactiveClass() if (item := SHARED.project.tree[tHandle]) else 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) logger.debug("Syntax highlighter enabled for item '%s'", tHandle)
return return
@@ -397,7 +388,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
elif text.startswith("["): # Special Command elif text.startswith("["): # Special Command
self.setCurrentBlockState(BLOCK_TEXT) self.setCurrentBlockState(BLOCK_TEXT)
hRules = self._txtRules hRules = self._txtRules if self._isNovel else self._minRules
sText = text.rstrip().lower() sText = text.rstrip().lower()
if sText in ("[newpage]", "[new page]", "[vspace]"): if sText in ("[newpage]", "[new page]", "[vspace]"):
@@ -414,7 +405,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
else: # Text Paragraph else: # Text Paragraph
self.setCurrentBlockState(BLOCK_TEXT) self.setCurrentBlockState(BLOCK_TEXT)
hRules = self._txtRules hRules = self._txtRules if self._isNovel else self._minRules
if hRules: if hRules:
for rX, hRule in hRules: for rX, hRule in hRules:
+6 -1
View File
@@ -165,6 +165,8 @@ class GuiDocViewer(QTextBrowser):
self._docTheme.keyword = SHARED.theme.colKey self._docTheme.keyword = SHARED.theme.colKey
self._docTheme.tag = SHARED.theme.colTag self._docTheme.tag = SHARED.theme.colTag
self._docTheme.optional = SHARED.theme.colOpt self._docTheme.optional = SHARED.theme.colOpt
self._docTheme.dialog = SHARED.theme.colDialN
self._docTheme.altdialog = SHARED.theme.colDialA
# Set default text margins # Set default text margins
self.document().setDocumentMargin(0) self.document().setDocumentMargin(0)
@@ -201,6 +203,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.initDocument(CONFIG.textFont, self._docTheme) qDoc.initDocument(CONFIG.textFont, self._docTheme)
qDoc.setKeywords(True) qDoc.setKeywords(True)
qDoc.setComments(CONFIG.viewComments) qDoc.setComments(CONFIG.viewComments)
@@ -361,8 +364,10 @@ class GuiDocViewer(QTextBrowser):
"""Process a clicked link in the document.""" """Process a clicked link in the document."""
if link := url.url(): if link := url.url():
logger.debug("Clicked link: '%s'", link) 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) self.loadDocumentTagRequest.emit(bits[2], nwDocMode.VIEW)
else:
self.navigateTo(link)
return return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
+113
View File
@@ -0,0 +1,113 @@
"""
novelWriter Text Pattern Functions
====================================
File History:
Created: 2024-06-01 [2.5ec1]
This file is a part of novelWriter
Copyright 20182024, 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()
+2
View File
@@ -341,6 +341,8 @@ class GuiManuscript(NToolDialog):
theme.keyword = QColor(245, 135, 31) theme.keyword = QColor(245, 135, 31)
theme.tag = QColor(66, 113, 174) theme.tag = QColor(66, 113, 174)
theme.optional = 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)) self.docPreview.beginNewBuild(len(docBuild))
for step, _ in docBuild.iterBuildPreview(theme): for step, _ in docBuild.iterBuildPreview(theme):
+4
View File
@@ -1093,11 +1093,13 @@ class _FormatTab(NScrollableForm):
self.stripUnicode = NSwitch(self, height=iPx) self.stripUnicode = NSwitch(self, height=iPx)
self.replaceTabs = NSwitch(self, height=iPx) self.replaceTabs = NSwitch(self, height=iPx)
self.keepBreaks = 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.justifyText"), self.justifyText)
self.addRow(self._build.getLabel("format.stripUnicode"), self.stripUnicode) self.addRow(self._build.getLabel("format.stripUnicode"), self.stripUnicode)
self.addRow(self._build.getLabel("format.replaceTabs"), self.replaceTabs) self.addRow(self._build.getLabel("format.replaceTabs"), self.replaceTabs)
self.addRow(self._build.getLabel("format.keepBreaks"), self.keepBreaks) self.addRow(self._build.getLabel("format.keepBreaks"), self.keepBreaks)
self.addRow(self._build.getLabel("format.showDialogue"), self.showDialogue)
# First Line Indent # First Line Indent
# ================= # =================
@@ -1180,6 +1182,7 @@ class _FormatTab(NScrollableForm):
self.stripUnicode.setChecked(self._build.getBool("format.stripUnicode")) self.stripUnicode.setChecked(self._build.getBool("format.stripUnicode"))
self.replaceTabs.setChecked(self._build.getBool("format.replaceTabs")) self.replaceTabs.setChecked(self._build.getBool("format.replaceTabs"))
self.keepBreaks.setChecked(self._build.getBool("format.keepBreaks")) 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.firstIndent.setChecked(self._build.getBool("format.firstLineIndent"))
self.indentWidth.setValue(self._build.getFloat("format.firstIndentWidth")) 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.stripUnicode", self.stripUnicode.isChecked())
self._build.setValue("format.replaceTabs", self.replaceTabs.isChecked()) self._build.setValue("format.replaceTabs", self.replaceTabs.isChecked())
self._build.setValue("format.keepBreaks", self.keepBreaks.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.firstLineIndent", self.firstIndent.isChecked())
self._build.setValue("format.firstIndentWidth", self.indentWidth.value()) self._build.setValue("format.firstIndentWidth", self.indentWidth.value())
@@ -19,6 +19,8 @@ mark {background: rgb(255, 255, 166);}
.break {text-align: left;} .break {text-align: left;}
.synopsis {font-style: italic;} .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);}
</style> </style>
<body> <body>
<article> <article>
@@ -2,8 +2,8 @@
"meta": { "meta": {
"projectName": "Lorem Ipsum", "projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com", "novelAuthor": "lipsum.com",
"buildTime": 1716803284, "buildTime": 1718057434,
"buildTimeStr": "2024-05-27 11:48:04" "buildTimeStr": "2024-06-11 00:10:34"
}, },
"text": { "text": {
"css": [ "css": [
@@ -20,7 +20,9 @@
".keyword {color: rgb(245, 135, 31); font-weight: bold;}", ".keyword {color: rgb(245, 135, 31); font-weight: bold;}",
".break {text-align: left;}", ".break {text-align: left;}",
".synopsis {font-style: italic;}", ".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": [ "html": [
[ [
+23
View File
@@ -24,6 +24,7 @@ import json
import pytest import pytest
from novelwriter import CONFIG
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
@@ -260,6 +261,28 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
"<a class='tag' href='#tag_Europe'>Europe</a></p>\n" "<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 # Footnotes
# ========= # =========
+125 -66
View File
@@ -26,6 +26,7 @@ import pytest
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from novelwriter import CONFIG
from novelwriter.constants import nwHeadFmt from novelwriter.constants import nwHeadFmt
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import HeadingFormatter, Tokenizer, stripEscape from novelwriter.core.tokenizer import HeadingFormatter, Tokenizer, stripEscape
@@ -1016,88 +1017,151 @@ def testCoreToken_TextFormat(mockGUI):
# Text Emphasis # Text Emphasis
tokens._text = "Some **bolded text** on this lines\n" tokens._text = "Some **bolded text** on this lines\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._tokens == [ assert tokens._tokens == [(
( Tokenizer.T_TEXT, 0, "Some bolded text on this lines",
Tokenizer.T_TEXT, 0, [
"Some bolded text on this lines", (5, Tokenizer.FMT_B_B, ""),
[ (16, Tokenizer.FMT_B_E, ""),
(5, Tokenizer.FMT_B_B, ""), ],
(16, Tokenizer.FMT_B_E, ""), Tokenizer.A_NONE
], )]
Tokenizer.A_NONE
),
]
assert tokens.allMarkdown[-1] == "Some **bolded text** on this lines\n\n" assert tokens.allMarkdown[-1] == "Some **bolded text** on this lines\n\n"
tokens._text = "Some _italic text_ on this lines\n" tokens._text = "Some _italic text_ on this lines\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._tokens == [ assert tokens._tokens == [(
( Tokenizer.T_TEXT, 0, "Some italic text on this lines",
Tokenizer.T_TEXT, 0, [
"Some italic text on this lines", (5, Tokenizer.FMT_I_B, ""),
[ (16, Tokenizer.FMT_I_E, ""),
(5, Tokenizer.FMT_I_B, ""), ],
(16, Tokenizer.FMT_I_E, ""), Tokenizer.A_NONE
], )]
Tokenizer.A_NONE
),
]
assert tokens.allMarkdown[-1] == "Some _italic text_ on this lines\n\n" assert tokens.allMarkdown[-1] == "Some _italic text_ on this lines\n\n"
tokens._text = "Some **_bold italic text_** on this lines\n" tokens._text = "Some **_bold italic text_** on this lines\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._tokens == [ assert tokens._tokens == [(
( Tokenizer.T_TEXT, 0, "Some bold italic text on this lines",
Tokenizer.T_TEXT, 0, [
"Some bold italic text on this lines", (5, Tokenizer.FMT_B_B, ""),
[ (5, Tokenizer.FMT_I_B, ""),
(5, Tokenizer.FMT_B_B, ""), (21, Tokenizer.FMT_I_E, ""),
(5, Tokenizer.FMT_I_B, ""), (21, Tokenizer.FMT_B_E, ""),
(21, Tokenizer.FMT_I_E, ""), ],
(21, Tokenizer.FMT_B_E, ""), Tokenizer.A_NONE
], )]
Tokenizer.A_NONE
),
]
assert tokens.allMarkdown[-1] == "Some **_bold italic text_** on this lines\n\n" assert tokens.allMarkdown[-1] == "Some **_bold italic text_** on this lines\n\n"
tokens._text = "Some ~~strikethrough text~~ on this lines\n" tokens._text = "Some ~~strikethrough text~~ on this lines\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._tokens == [ assert tokens._tokens == [(
( Tokenizer.T_TEXT, 0, "Some strikethrough text on this lines",
Tokenizer.T_TEXT, 0, [
"Some strikethrough text on this lines", (5, Tokenizer.FMT_D_B, ""),
[ (23, Tokenizer.FMT_D_E, ""),
(5, Tokenizer.FMT_D_B, ""), ],
(23, Tokenizer.FMT_D_E, ""), Tokenizer.A_NONE
], )]
Tokenizer.A_NONE
),
]
assert tokens.allMarkdown[-1] == "Some ~~strikethrough text~~ on this lines\n\n" 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._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._tokens == [ assert tokens._tokens == [(
( Tokenizer.T_TEXT, 0, "Some nested bold and italic and strikethrough text here",
Tokenizer.T_TEXT, 0, [
"Some nested bold and italic and strikethrough text here", (5, Tokenizer.FMT_B_B, ""),
[ (21, Tokenizer.FMT_I_B, ""),
(5, Tokenizer.FMT_B_B, ""), (27, Tokenizer.FMT_I_E, ""),
(21, Tokenizer.FMT_I_B, ""), (32, Tokenizer.FMT_D_B, ""),
(27, Tokenizer.FMT_I_E, ""), (45, Tokenizer.FMT_D_E, ""),
(32, Tokenizer.FMT_D_B, ""), (50, Tokenizer.FMT_B_E, ""),
(45, Tokenizer.FMT_D_E, ""), ],
(50, Tokenizer.FMT_B_E, ""), Tokenizer.A_NONE
], )]
Tokenizer.A_NONE
),
]
assert tokens.allMarkdown[-1] == ( assert tokens.allMarkdown[-1] == (
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" "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 @pytest.mark.core
def testCoreToken_SpecialFormat(mockGUI): def testCoreToken_SpecialFormat(mockGUI):
"""Test the tokenization of special formats in the Tokenizer class.""" """Test the tokenization of special formats in the Tokenizer class."""
@@ -1267,11 +1331,6 @@ def testCoreToken_ProcessHeaders(mockGUI):
project.data.setLanguage("en") project.data.setLanguage("en")
project._loadProjectLocalisation() project._loadProjectLocalisation()
tokens = BareTokenizer(project) tokens = BareTokenizer(project)
##
# Story Files
##
tokens._isNovel = True tokens._isNovel = True
# Titles # Titles
+49 -2
View File
@@ -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 @pytest.mark.core
def testCoreToOdt_ConvertHeaders(mockGUI): def testCoreToOdt_ConvertHeaders(mockGUI):
"""Test the converter of the ToOdt class.""" """Test the converter of the ToOdt class."""
@@ -854,8 +890,8 @@ def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths):
@pytest.mark.core @pytest.mark.core
def testCoreToOdt_Format(mockGUI): def testCoreToOdt_SpecialFormats(mockGUI):
"""Test the formatters for the ToOdt class.""" """Test the special formatters for the ToOdt class."""
project = NWProject() project = NWProject()
odt = ToOdt(project, isFlat=True) odt = ToOdt(project, isFlat=True)
@@ -1168,6 +1204,17 @@ def testCoreToOdt_ODTTextStyle():
txtStyle.setFontStyle("stuff") txtStyle.setFontStyle("stuff")
assert txtStyle._tAttr["font-style"] == ["fo", None] 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 # Background Color
assert txtStyle._tAttr["background-color"] == ["fo", None] assert txtStyle._tAttr["background-color"] == ["fo", None]
txtStyle.setBackgroundColour("stuff") txtStyle.setBackgroundColour("stuff")
+31 -1
View File
@@ -44,6 +44,8 @@ THEME.modifier = QColor(129, 55, 9)
THEME.keyword = QColor(245, 135, 31) THEME.keyword = QColor(245, 135, 31)
THEME.tag = QColor(66, 113, 174) THEME.tag = QColor(66, 113, 174)
THEME.optional = 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: def charFmtInBlock(block: QTextBlock, pos: int) -> QTextCharFormat:
@@ -441,11 +443,17 @@ def testCoreToQTextDocument_TextBlockFormats(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToQTextDocument_TextCharFormats(mockGUI): def testCoreToQTextDocument_TextCharFormats(mockGUI):
"""Test text char formats in the ToQTextDocument class.""" """Test text char formats in the ToQTextDocument class."""
CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
CONFIG.altDialogOpen = "|<"
CONFIG.altDialogClose = ">|"
project = NWProject() project = NWProject()
qdoc = ToQTextDocument(project) qdoc = ToQTextDocument(project)
# Convert before init # Convert before init
qdoc._text = "Blabla" qdoc._text = "Blabla"
qdoc.setDialogueHighlight(True)
qdoc.doConvert() qdoc.doConvert()
qdoc.tokenizeText() qdoc.tokenizeText()
assert qdoc.document.toPlainText() == "" assert qdoc.document.toPlainText() == ""
@@ -465,10 +473,12 @@ def testCoreToQTextDocument_TextCharFormats(mockGUI):
"With [m]highlighted[/m] text\n\n" "With [m]highlighted[/m] text\n\n"
"With super[sup]script[/sup] text\n\n" "With super[sup]script[/sup] text\n\n"
"With sub[sub]script[/sub] 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.tokenizeText()
qdoc.doConvert() qdoc.doConvert()
assert qdoc.document.blockCount() == 8 assert qdoc.document.blockCount() == 10
# 0: Scene # 0: Scene
block = qdoc.document.findBlockByNumber(0) block = qdoc.document.findBlockByNumber(0)
@@ -544,6 +554,26 @@ def testCoreToQTextDocument_TextCharFormats(mockGUI):
cFmt = charFmtInBlock(block, 15) cFmt = charFmtInBlock(block, 15)
assert cFmt.verticalAlignment() == QtVAlignNormal 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 @pytest.mark.core
def testCoreToQTextDocument_Footnotes(mockGUI): def testCoreToQTextDocument_Footnotes(mockGUI):
+5
View File
@@ -160,6 +160,11 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
docViewer._linkClicked(QUrl("#tag_bod")) docViewer._linkClicked(QUrl("#tag_bod"))
assert docViewer.docHandle == "4c4f28287af27" 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 # Click mouse nav buttons
qtbot.mouseClick(docViewer.viewport(), Qt.BackButton, pos=rect.center(), delay=100) qtbot.mouseClick(docViewer.viewport(), Qt.BackButton, pos=rect.center(), delay=100)
assert docViewer.docHandle == "88243afbe5ed8" assert docViewer.docHandle == "88243afbe5ed8"
-3
View File
@@ -64,9 +64,6 @@ def testGuiI18n_Localisation(qtbot, monkeypatch, language, nwGUI, projPath):
qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(dType) is not None, timeout=1000) qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(dType) is not None, timeout=1000)
dialog = SHARED.findTopLevelWidget(dType) dialog = SHARED.findTopLevelWidget(dType)
assert isinstance(dialog, dType) assert isinstance(dialog, dType)
with qtbot.waitExposed(dialog):
dialog.show()
dialog.close()
showDialog(nwGUI.showWelcomeDialog, GuiWelcome) showDialog(nwGUI.showWelcomeDialog, GuiWelcome)
showDialog(nwGUI.showPreferencesDialog, GuiPreferences) showDialog(nwGUI.showPreferencesDialog, GuiPreferences)
+272
View File
@@ -0,0 +1,272 @@
"""
novelWriter Patterns Module Tester
====================================
This file is a part of novelWriter
Copyright 20182024, 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.justifyText", False)
build.setValue("format.stripUnicode", False) build.setValue("format.stripUnicode", False)
build.setValue("format.replaceTabs", 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.pageUnit", "mm")
build.setValue("format.pageSize", "Custom") build.setValue("format.pageSize", "Custom")
@@ -580,6 +586,8 @@ def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
assert fmtTab.justifyText.isChecked() is False assert fmtTab.justifyText.isChecked() is False
assert fmtTab.stripUnicode.isChecked() is False assert fmtTab.stripUnicode.isChecked() is False
assert fmtTab.replaceTabs.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.firstIndent.isChecked() is False
assert fmtTab.indentWidth.value() == 1.4 assert fmtTab.indentWidth.value() == 1.4
@@ -603,6 +611,8 @@ def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
fmtTab.justifyText.setChecked(True) fmtTab.justifyText.setChecked(True)
fmtTab.stripUnicode.setChecked(True) fmtTab.stripUnicode.setChecked(True)
fmtTab.replaceTabs.setChecked(True) fmtTab.replaceTabs.setChecked(True)
fmtTab.keepBreaks.setChecked(False)
fmtTab.showDialogue.setChecked(True)
fmtTab.firstIndent.setChecked(True) fmtTab.firstIndent.setChecked(True)
fmtTab.indentWidth.setValue(2.0) 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.justifyText") is True
assert build.getBool("format.stripUnicode") is True assert build.getBool("format.stripUnicode") is True
assert build.getBool("format.replaceTabs") 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.getBool("format.firstLineIndent") is True
assert build.getFloat("format.firstIndentWidth") == 2.0 assert build.getFloat("format.firstIndentWidth") == 2.0