Skip to content

Commit

Permalink
Extend dialogue highlighting (#1908)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo authored Jun 10, 2024
2 parents 9bcb247 + 7821f09 commit cc61c53
Show file tree
Hide file tree
Showing 23 changed files with 810 additions and 154 deletions.
8 changes: 4 additions & 4 deletions novelwriter/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ class nwConst:
class nwRegEx:

FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:i|b|s|u|m|sup|sub)\])"
FMT_SV = r"(?<!\\)(\[(?i)(?:footnote):)(.+?)(?<!\\)(\])"
FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])"
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"


class nwShortcode:
Expand Down
2 changes: 2 additions & 0 deletions novelwriter/core/buildsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"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),
Expand Down Expand Up @@ -131,6 +132,7 @@
"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"),
Expand Down
1 change: 1 addition & 0 deletions novelwriter/core/docbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ def _setupBuild(self, bldObj: Tokenizer) -> dict:
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"),
Expand Down
8 changes: 7 additions & 1 deletion novelwriter/core/tohtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
Tokenizer.FMT_SUP_E: "</sup>",
Tokenizer.FMT_SUB_B: "<sub>",
Tokenizer.FMT_SUB_E: "</sub>",
Tokenizer.FMT_DL_B: "<span class='dialog'>",
Tokenizer.FMT_DL_E: "</span>",
Tokenizer.FMT_ADL_B: "<span class='altdialog'>",
Tokenizer.FMT_ADL_E: "</span>",
Tokenizer.FMT_STRIP: "",
}

Expand Down Expand Up @@ -431,6 +435,8 @@ def getStyleSheet(self) -> list[str]:
styles.append(".break {text-align: left;}")
styles.append(".synopsis {font-style: italic;}")
styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
styles.append(".dialog {color: rgb(66, 113, 174);}")
styles.append(".altdialog {color: rgb(129, 55, 9);}")

return styles

Expand All @@ -451,7 +457,7 @@ def _formatText(self, text: str, tFmt: T_Formats) -> str:
else:
html = "<sup>ERR</sup>"
else:
html = HTML5_TAGS.get(fmt, "ERR")
html = HTML5_TAGS.get(fmt, "")
temp = f"{temp[:pos]}{html}{temp[pos:]}"
temp = temp.replace("\n", "<br>")
return stripEscape(temp)
Expand Down
61 changes: 50 additions & 11 deletions novelwriter/core/tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@
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__)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -193,7 +197,8 @@ def __init__(self, project: NWProject) -> None:

# 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
Expand All @@ -208,12 +213,12 @@ def __init__(self, project: NWProject) -> None:

# 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,
Expand All @@ -228,6 +233,8 @@ def __init__(self, project: NWProject) -> None:
nwShortcode.FOOTNOTE_B: self.FMT_FNOTE,
}

self._rxDialogue: list[tuple[QRegularExpression, int, int]] = []

return

##
Expand Down Expand Up @@ -349,6 +356,29 @@ def setJustify(self, state: bool) -> None:
self._doJustify = state
return

def setDialogueHighlight(self, state: bool) -> None:
"""Enable or disable dialogue highlighting."""
self._rxDialogue = []
self._showDialog = state
if state:
if CONFIG.dialogStyle > 0:
self._rxDialogue.append((
REGEX_PATTERNS.dialogStyle, self.FMT_DL_B, self.FMT_DL_E
))
if CONFIG.dialogLine:
self._rxDialogue.append((
REGEX_PATTERNS.dialogLine, self.FMT_DL_B, self.FMT_DL_E
))
if CONFIG.narratorBreak:
self._rxDialogue.append((
REGEX_PATTERNS.narratorBreak, self.FMT_DL_E, self.FMT_DL_B
))
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
self._rxDialogue.append((
REGEX_PATTERNS.altDialogStyle, self.FMT_ADL_B, self.FMT_ADL_E
))
return

def setTitleMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower title margin."""
self._marginTitle = (float(upper), float(lower))
Expand Down Expand Up @@ -1106,6 +1136,15 @@ def _extractFormats(self, text: str, skip: int = 0) -> tuple[str, T_Formats]:
f"{tHandle}:{rxMatch.captured(2)}",
))

# Match Dialogue
if self._rxDialogue:
for regEx, fmtB, fmtE in self._rxDialogue:
rxItt = regEx.globalMatch(text, 0)
while rxItt.hasNext():
rxMatch = rxItt.next()
temp.append((rxMatch.capturedStart(0), 0, fmtB, ""))
temp.append((rxMatch.capturedEnd(0), 0, fmtE, ""))

# Post-process text and format
result = text
formats = []
Expand Down
59 changes: 45 additions & 14 deletions novelwriter/core/toodt.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ def _mkTag(ns: str, tag: str) -> str:
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
Expand All @@ -98,6 +100,8 @@ def _mkTag(ns: str, tag: str) -> str:
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"
Expand Down Expand Up @@ -216,13 +220,15 @@ def __init__(self, project: NWProject, isFlat: bool) -> None:
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

Expand Down Expand Up @@ -324,6 +330,10 @@ def initDocument(self) -> None:
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)
Expand Down Expand Up @@ -684,6 +694,14 @@ def _addTextPar(
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:
Expand Down Expand Up @@ -757,6 +775,10 @@ def _textStyle(self, hFmt: int) -> str:
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
Expand Down Expand Up @@ -1357,6 +1379,7 @@ def __init__(self, name: str) -> None:
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],
Expand Down Expand Up @@ -1391,6 +1414,14 @@ def setFontStyle(self, value: str | None) -> None:
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] == "#":
Expand Down
28 changes: 19 additions & 9 deletions novelwriter/core/toqdoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,18 @@


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:
Expand Down Expand Up @@ -340,6 +342,14 @@ def _insertFragments(
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)
Expand Down
3 changes: 3 additions & 0 deletions novelwriter/gui/doceditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2030,6 +2030,9 @@ def _docAutoReplace(self, text: str) -> None:
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
Expand Down
Loading

0 comments on commit cc61c53

Please sign in to comment.