From 80811ebf4450efd19521f8112f1db3a6fff01cb8 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Jun 2024 12:41:42 +0200
Subject: [PATCH 01/15] Don't highlight dialogue in notes
---
novelwriter/gui/dochighlight.py | 27 ++++++++++++++++++++-------
1 file changed, 20 insertions(+), 7 deletions(-)
diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py
index c00f7b44..c51a1d90 100644
--- a/novelwriter/gui/dochighlight.py
+++ b/novelwriter/gui/dochighlight.py
@@ -59,8 +59,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 +69,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 +139,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,6 +149,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
hlRule = {
0: self._hStyles["nobreak"],
}
+ self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
@@ -204,6 +208,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
2: self._hStyles["italic"],
3: self._hStyles["markup"],
}
+ self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
@@ -215,6 +220,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
2: self._hStyles["bold"],
3: self._hStyles["markup"],
}
+ self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
@@ -226,6 +232,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
2: self._hStyles["strike"],
3: self._hStyles["markup"],
}
+ self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
@@ -235,6 +242,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
hlRule = {
1: self._hStyles["code"],
}
+ self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
@@ -246,6 +254,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
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 +264,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
hlRule = {
1: self._hStyles["markup"],
}
+ self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule))
# Auto-Replace Tags
@@ -263,6 +273,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 +291,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 +410,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 +427,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:
From 388141aae61befe47f138bb490358518667787d9 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Jun 2024 13:06:46 +0200
Subject: [PATCH 02/15] Move text regex patterns to a separate class
---
novelwriter/gui/dochighlight.py | 42 +++---------
novelwriter/text/patterns.py | 113 ++++++++++++++++++++++++++++++++
2 files changed, 123 insertions(+), 32 deletions(-)
create mode 100644 novelwriter/text/patterns.py
diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py
index c51a1d90..bb2cfc49 100644
--- a/novelwriter/gui/dochighlight.py
+++ b/novelwriter/gui/dochighlight.py
@@ -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__)
@@ -155,54 +156,35 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# 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"],
@@ -213,8 +195,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
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"],
@@ -225,8 +206,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
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"],
@@ -237,8 +217,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._cmnRules.append((rxRule, hlRule))
# Shortcodes
- rxRule = QRegularExpression(nwRegEx.FMT_SC)
- rxRule.setPatternOptions(QRegExUnicode)
+ rxRule = REGEX_PATTERNS.shortcodePlain
hlRule = {
1: self._hStyles["code"],
}
@@ -247,8 +226,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
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"],
diff --git a/novelwriter/text/patterns.py b/novelwriter/text/patterns.py
new file mode 100644
index 00000000..5bbf237c
--- /dev/null
+++ b/novelwriter/text/patterns.py
@@ -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 .
+"""
+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"({sym}\\b)(.*?)(\\b{sym})")
+ 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()
From 2e143071d9f736fff8240e0b495f00acb8a1a663 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Jun 2024 13:34:59 +0200
Subject: [PATCH 03/15] Add dialogue processing to the tokenizer
---
novelwriter/core/tokenizer.py | 60 +++++++++++++++++++++++++++++------
novelwriter/text/patterns.py | 2 +-
2 files changed, 51 insertions(+), 11 deletions(-)
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index 314d14a8..10abd1fc 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -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)
@@ -208,12 +212,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 +232,8 @@ class Tokenizer(ABC):
nwShortcode.FOOTNOTE_B: self.FMT_FNOTE,
}
+ self._rxDialogue: list[tuple[QRegularExpression, int, int]] = []
+
return
##
@@ -349,6 +355,28 @@ class Tokenizer(ABC):
self._doJustify = state
return
+ def setDialogueHighlight(self, state: bool) -> None:
+ """Enable or disable dialogue highlighting."""
+ self._rxDialogue = []
+ 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 +1134,18 @@ 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)
+ rxMatch = regEx.match(text, 0)
+ if rxMatch.hasMatch():
+ temp.append((rxMatch.capturedStart(0), 0, fmtB, ""))
+ temp.append((rxMatch.capturedEnd(0), 0, fmtE, ""))
+
+ print(text)
+ print(temp)
+
# Post-process text and format
result = text
formats = []
diff --git a/novelwriter/text/patterns.py b/novelwriter/text/patterns.py
index 5bbf237c..2965b1c9 100644
--- a/novelwriter/text/patterns.py
+++ b/novelwriter/text/patterns.py
@@ -96,7 +96,7 @@ class RegExPatterns:
def narratorBreak(self) -> QRegularExpression:
"""Dialogue narrator break rule based on user settings."""
sym = QRegularExpression.escape(CONFIG.narratorBreak)
- rxRule = QRegularExpression(f"({sym}\\b)(.*?)(\\b{sym})")
+ rxRule = QRegularExpression(f"{sym}\\b.*?\\b{sym}")
rxRule.setPatternOptions(QRegExUnicode)
return rxRule
From 987740f7e9f03fbfa360358365725dca99715964 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Jun 2024 13:35:22 +0200
Subject: [PATCH 04/15] Add dialogue highlighting to the viewer
---
novelwriter/core/toqdoc.py | 28 +++++++++++++++++++---------
novelwriter/gui/docviewer.py | 3 +++
2 files changed, 22 insertions(+), 9 deletions(-)
diff --git a/novelwriter/core/toqdoc.py b/novelwriter/core/toqdoc.py
index d40c4345..fc7d3b46 100644
--- a/novelwriter/core/toqdoc.py
+++ b/novelwriter/core/toqdoc.py
@@ -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)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index d73f0abb..63eb3edb 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -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)
From d190315909cb9eedef3b286af6a87bfe663b364d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Jun 2024 14:16:05 +0200
Subject: [PATCH 05/15] Make a few improvements to regex and remove debug
output
---
novelwriter/core/tokenizer.py | 7 +------
novelwriter/gui/doceditor.py | 3 +++
novelwriter/text/patterns.py | 2 +-
3 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index 10abd1fc..d33d5b62 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -1137,15 +1137,10 @@ class Tokenizer(ABC):
# Match Dialogue
if self._rxDialogue:
for regEx, fmtB, fmtE in self._rxDialogue:
- rxItt = regEx.globalMatch(text, 0)
- rxMatch = regEx.match(text, 0)
- if rxMatch.hasMatch():
+ if (rxMatch := regEx.match(text, 0)).hasMatch():
temp.append((rxMatch.capturedStart(0), 0, fmtB, ""))
temp.append((rxMatch.capturedEnd(0), 0, fmtE, ""))
- print(text)
- print(temp)
-
# Post-process text and format
result = text
formats = []
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 6b16f021..3de5af24 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -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
diff --git a/novelwriter/text/patterns.py b/novelwriter/text/patterns.py
index 2965b1c9..094b287c 100644
--- a/novelwriter/text/patterns.py
+++ b/novelwriter/text/patterns.py
@@ -96,7 +96,7 @@ class RegExPatterns:
def narratorBreak(self) -> QRegularExpression:
"""Dialogue narrator break rule based on user settings."""
sym = QRegularExpression.escape(CONFIG.narratorBreak)
- rxRule = QRegularExpression(f"{sym}\\b.*?\\b{sym}")
+ rxRule = QRegularExpression(f"\\B{sym}\\S.*?\\S{sym}\\B")
rxRule.setPatternOptions(QRegExUnicode)
return rxRule
From 8fc97c263bea1c136c0ff6fc724449962ba2b38c Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Jun 2024 18:18:21 +0200
Subject: [PATCH 06/15] Try to fix 3.10 test seg fault
---
tests/test_gui/test_gui_i18n.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/tests/test_gui/test_gui_i18n.py b/tests/test_gui/test_gui_i18n.py
index 93f333a7..83638f13 100644
--- a/tests/test_gui/test_gui_i18n.py
+++ b/tests/test_gui/test_gui_i18n.py
@@ -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)
From 92b71683639e38a2fe4a73fff823526e71939ec6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 1 Jun 2024 23:03:30 +0200
Subject: [PATCH 07/15] Add tests for patterns
---
novelwriter/constants.py | 8 +-
novelwriter/text/patterns.py | 2 +-
tests/test_text/test_text_patterns.py | 272 ++++++++++++++++++++++++++
3 files changed, 277 insertions(+), 5 deletions(-)
create mode 100644 tests/test_text/test_text_patterns.py
diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index ebaef80c..64ffb900 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -64,10 +64,10 @@ class nwConst:
class nwRegEx:
FMT_EI = r"(?.
+"""
+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)]
+ ]
From 7744ae591271a9c10b559e8742607c0a2baf9abd Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 2 Jun 2024 23:36:28 +0200
Subject: [PATCH 08/15] Fix footnote navigation in viewer
---
novelwriter/gui/docviewer.py | 4 +++-
tests/test_gui/test_gui_docviewer.py | 5 +++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 63eb3edb..20e1888b 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -364,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")
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index b5928209..b99069a9 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -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"
From d48f1fa2928fc2a809e844dd35339eaf6bd569f5 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 2 Jun 2024 23:48:42 +0200
Subject: [PATCH 09/15] Update QTextDocument test
---
tests/test_core/test_core_toqdoc.py | 32 ++++++++++++++++++++++++++++-
1 file changed, 31 insertions(+), 1 deletion(-)
diff --git a/tests/test_core/test_core_toqdoc.py b/tests/test_core/test_core_toqdoc.py
index bc83b1af..b83badbe 100644
--- a/tests/test_core/test_core_toqdoc.py
+++ b/tests/test_core/test_core_toqdoc.py
@@ -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 || 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 || 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):
From f61ce3919b59f0f4057c0d8788dafb20cfb3970e Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 10 Jun 2024 17:01:40 +0200
Subject: [PATCH 10/15] Add highlight option to build settings and preview
---
novelwriter/core/buildsettings.py | 2 ++
novelwriter/core/docbuild.py | 1 +
novelwriter/tools/manuscript.py | 2 ++
novelwriter/tools/manussettings.py | 4 ++++
tests/test_tools/test_tools_manussettings.py | 12 ++++++++++++
5 files changed, 21 insertions(+)
diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py
index aa295b6b..72fd395b 100644
--- a/novelwriter/core/buildsettings.py
+++ b/novelwriter/core/buildsettings.py
@@ -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"),
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index a6d47740..fa078cbb 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -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"),
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 85105b86..1e19187b 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -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(174, 0, 0)
+ theme.altdialog = QColor(66, 113, 174)
self.docPreview.beginNewBuild(len(docBuild))
for step, _ in docBuild.iterBuildPreview(theme):
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index 23b2a946..f060a7b0 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -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())
diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py
index 76a41a48..cc39745e 100644
--- a/tests/test_tools/test_tools_manussettings.py
+++ b/tests/test_tools/test_tools_manussettings.py
@@ -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
From f0f5ae6dc9103cdea6df351264daa9997d883d2e Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 10 Jun 2024 17:18:03 +0200
Subject: [PATCH 11/15] Add support for dialogue highlighting in HTML output
---
novelwriter/core/tohtml.py | 6 +++++
.../mBuildDocBuild_HTML5_Lorem_Ipsum.htm | 2 ++
.../mBuildDocBuild_HTML5_Lorem_Ipsum.json | 8 ++++---
tests/test_core/test_core_tohtml.py | 23 +++++++++++++++++++
4 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index a9ba0bb7..5805f240 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -52,6 +52,10 @@ HTML5_TAGS = {
Tokenizer.FMT_SUP_E: "",
Tokenizer.FMT_SUB_B: "",
Tokenizer.FMT_SUB_E: "",
+ Tokenizer.FMT_DL_B: "",
+ Tokenizer.FMT_DL_E: "",
+ Tokenizer.FMT_ADL_B: "",
+ Tokenizer.FMT_ADL_E: "",
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(174, 0, 0);}")
+ styles.append(".altdialog {color: rgb(66, 113, 174);}")
return styles
diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
index abcaa5f4..04b56dae 100644
--- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
+++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
@@ -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(174, 0, 0);}
+.altdialog {color: rgb(66, 113, 174);}
diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json
index 8eac4a6b..f59579d2 100644
--- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json
+++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json
@@ -2,8 +2,8 @@
"meta": {
"projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
- "buildTime": 1716803284,
- "buildTimeStr": "2024-05-27 11:48:04"
+ "buildTime": 1718032228,
+ "buildTimeStr": "2024-06-10 17:10:28"
},
"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(174, 0, 0);}",
+ ".altdialog {color: rgb(66, 113, 174);}"
],
"html": [
[
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index 8a4a54ab..9e150295 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -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):
"Europe
\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 == (
+ "Chapter
\n"
+ "This text \u201chas dialogue\u201d in it.
\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 == (
+ "Chapter
\n"
+ "This text :: has alt dialogue :: in it.
\n"
+ )
+
# Footnotes
# =========
From 0b3488c2657def11d3d77f7cc01a34b73179aa49 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 10 Jun 2024 23:12:54 +0200
Subject: [PATCH 12/15] Add processing of dialogue highlight for ODT files
---
novelwriter/core/tokenizer.py | 4 ++-
novelwriter/core/toodt.py | 59 ++++++++++++++++++++++++++---------
2 files changed, 48 insertions(+), 15 deletions(-)
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index b1c6b50b..b7bc1adc 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -197,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
@@ -358,6 +359,7 @@ class Tokenizer(ABC):
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((
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index c901db86..efb4f8d1 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -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] == "#":
From 416c580a45e86acfb529060ed0e3a4c6c54151b1 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 10 Jun 2024 23:50:59 +0200
Subject: [PATCH 13/15] Fix highlighting of multiple dialogue sections
---
novelwriter/core/tohtml.py | 2 +-
novelwriter/core/tokenizer.py | 4 +-
tests/test_core/test_core_tokenizer.py | 191 ++++++++++++++++---------
3 files changed, 129 insertions(+), 68 deletions(-)
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index 5805f240..446abdca 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -457,7 +457,7 @@ class ToHtml(Tokenizer):
else:
html = "ERR"
else:
- html = HTML5_TAGS.get(fmt, "ERR")
+ html = HTML5_TAGS.get(fmt, "")
temp = f"{temp[:pos]}{html}{temp[pos:]}"
temp = temp.replace("\n", "
")
return stripEscape(temp)
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index b7bc1adc..a2238b73 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -1139,7 +1139,9 @@ class Tokenizer(ABC):
# Match Dialogue
if self._rxDialogue:
for regEx, fmtB, fmtE in self._rxDialogue:
- if (rxMatch := regEx.match(text, 0)).hasMatch():
+ 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, ""))
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index 814a27be..aa23ad1d 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -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
From 4b0ad6ec48526e893a10335a7a8070a79fb831aa Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Jun 2024 00:03:54 +0200
Subject: [PATCH 14/15] Add test coverage of ODT dialogue
---
tests/test_core/test_core_toodt.py | 51 ++++++++++++++++++++++++++++--
1 file changed, 49 insertions(+), 2 deletions(-)
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index 4a341165..77f17a0d 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -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) == (
+ ''
+ 'Text with '
+ '\'dialogue in it.\''
+ ''
+ )
+
+ # 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) == (
+ ''
+ 'Text with '
+ '::dialogue in it.::'
+ ''
+ )
+
+
@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")
From 7821f099bf8cdc31e69b2ccb38b8eb7ac4e9b8cd Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 11 Jun 2024 00:11:44 +0200
Subject: [PATCH 15/15] Use consistent dialogue colours in preview, ODT and
HTML
---
novelwriter/core/tohtml.py | 4 ++--
novelwriter/tools/manuscript.py | 4 ++--
tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm | 4 ++--
tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json | 8 ++++----
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index 446abdca..ac336628 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -435,8 +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(174, 0, 0);}")
- styles.append(".altdialog {color: rgb(66, 113, 174);}")
+ styles.append(".dialog {color: rgb(66, 113, 174);}")
+ styles.append(".altdialog {color: rgb(129, 55, 9);}")
return styles
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 1e19187b..f43eab64 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -341,8 +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(174, 0, 0)
- theme.altdialog = 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):
diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
index 04b56dae..d5348e24 100644
--- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
+++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
@@ -19,8 +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(174, 0, 0);}
-.altdialog {color: rgb(66, 113, 174);}
+.dialog {color: rgb(66, 113, 174);}
+.altdialog {color: rgb(129, 55, 9);}
diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json
index f59579d2..ab8b2d8f 100644
--- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json
+++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json
@@ -2,8 +2,8 @@
"meta": {
"projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
- "buildTime": 1718032228,
- "buildTimeStr": "2024-06-10 17:10:28"
+ "buildTime": 1718057434,
+ "buildTimeStr": "2024-06-11 00:10:34"
},
"text": {
"css": [
@@ -21,8 +21,8 @@
".break {text-align: left;}",
".synopsis {font-style: italic;}",
".comment {font-style: italic; color: rgb(100, 100, 100);}",
- ".dialog {color: rgb(174, 0, 0);}",
- ".altdialog {color: rgb(66, 113, 174);}"
+ ".dialog {color: rgb(66, 113, 174);}",
+ ".altdialog {color: rgb(129, 55, 9);}"
],
"html": [
[