From bb2c2bb7fe8d52009c08110df42b06e8fecb6ebc Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 1 Mar 2024 22:09:50 +0100
Subject: [PATCH 1/9] Add preserve tabs setting to HTML builds
---
novelwriter/core/buildsettings.py | 2 ++
novelwriter/core/docbuild.py | 2 +-
novelwriter/tools/manussettings.py | 5 +++++
3 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py
index c2a04375..f1dbe57e 100644
--- a/novelwriter/core/buildsettings.py
+++ b/novelwriter/core/buildsettings.py
@@ -82,6 +82,7 @@ SETTINGS_TEMPLATE = {
"odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
"odt.pageCountOffset": (int, 0),
"html.addStyles": (bool, True),
+ "html.preserveTabs": (bool, False),
}
SETTINGS_LABELS = {
@@ -132,6 +133,7 @@ SETTINGS_LABELS = {
"html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"),
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
+ "html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve Tab Characters"),
}
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index 8d6a0f21..d5d8f176 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -161,7 +161,7 @@ class NWBuildDocument:
else:
yield i, False
- if self._build.getBool("format.replaceTabs"):
+ if not self._build.getBool("html.preserveTabs"):
makeObj.replaceTabs()
self._error = None
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index fe78f3b4..573e8483 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -1245,6 +1245,9 @@ class _OutputTab(NScrollableForm):
self.htmlAddStyles = NSwitch(self, height=iPx)
self.addRow(self._build.getLabel("html.addStyles"), self.htmlAddStyles)
+ self.htmlPreserveTabs = NSwitch(self, height=iPx)
+ self.addRow(self._build.getLabel("html.preserveTabs"), self.htmlPreserveTabs)
+
# Finalise
self.finalise()
@@ -1256,6 +1259,7 @@ class _OutputTab(NScrollableForm):
self.odtPageHeader.setText(self._build.getStr("odt.pageHeader"))
self.odtPageCountOffset.setValue(self._build.getInt("odt.pageCountOffset"))
self.htmlAddStyles.setChecked(self._build.getBool("html.addStyles"))
+ self.htmlPreserveTabs.setChecked(self._build.getBool("html.preserveTabs"))
return
def saveContent(self) -> None:
@@ -1264,6 +1268,7 @@ class _OutputTab(NScrollableForm):
self._build.setValue("odt.pageHeader", self.odtPageHeader.text())
self._build.setValue("odt.pageCountOffset", self.odtPageCountOffset.value())
self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked())
+ self._build.setValue("html.preserveTabs", self.htmlPreserveTabs.isChecked())
return
##
From ef1fc0943da55c2ccb25c5d9a2cb0829d4ff5093 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 1 Mar 2024 22:12:45 +0100
Subject: [PATCH 2/9] Add a way to ignore hard line breaks in Markdown builds
(#944)
---
novelwriter/core/buildsettings.py | 4 ++++
novelwriter/core/docbuild.py | 2 ++
novelwriter/core/tomd.py | 9 ++++++++-
novelwriter/tools/manussettings.py | 8 ++++++++
4 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py
index f1dbe57e..1037c572 100644
--- a/novelwriter/core/buildsettings.py
+++ b/novelwriter/core/buildsettings.py
@@ -81,6 +81,7 @@ SETTINGS_TEMPLATE = {
"odt.addColours": (bool, True),
"odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
"odt.pageCountOffset": (int, 0),
+ "md.preserveBreaks": (bool, True),
"html.addStyles": (bool, True),
"html.preserveTabs": (bool, False),
}
@@ -131,6 +132,9 @@ SETTINGS_LABELS = {
"odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
"odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
+ "md": QT_TRANSLATE_NOOP("Builds", "Markdown (.md)"),
+ "md.preserveBreaks": QT_TRANSLATE_NOOP("Builds", "Preserve Hard Line Breaks"),
+
"html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"),
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
"html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve Tab Characters"),
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index d5d8f176..924a038a 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -192,6 +192,8 @@ class NWBuildDocument:
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
+ makeObj.setPreserveBreaks(self._build.getBool("md.preserveBreaks"))
+
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
diff --git a/novelwriter/core/tomd.py b/novelwriter/core/tomd.py
index fbc2e463..a57ba18f 100644
--- a/novelwriter/core/tomd.py
+++ b/novelwriter/core/tomd.py
@@ -49,6 +49,7 @@ class ToMarkdown(Tokenizer):
super().__init__(project)
self._genMode = self.M_STD
self._fullMD: list[str] = []
+ self._preserveBreaks = True
return
##
@@ -74,6 +75,11 @@ class ToMarkdown(Tokenizer):
self._genMode = self.M_EXT
return
+ def setPreserveBreaks(self, state: bool) -> None:
+ """Preserve line breaks in paragraphs."""
+ self._preserveBreaks = state
+ return
+
##
# Class Methods
##
@@ -125,12 +131,13 @@ class ToMarkdown(Tokenizer):
para = []
lines = []
+ lineSep = " \n" if self._preserveBreaks else " "
for tType, _, tText, tFormat, tStyle in self._tokens:
if tType == self.T_EMPTY:
if len(para) > 0:
- tTemp = (" \n".join(para)).rstrip(" ")
+ tTemp = (lineSep.join(para)).rstrip(" ")
lines.append(f"{tTemp}\n\n")
para = []
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index 573e8483..3ccf79e1 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -1248,6 +1248,12 @@ class _OutputTab(NScrollableForm):
self.htmlPreserveTabs = NSwitch(self, height=iPx)
self.addRow(self._build.getLabel("html.preserveTabs"), self.htmlPreserveTabs)
+ # Markdown Document
+ self.addGroupLabel(self._build.getLabel("md"))
+
+ self.mdPreserveBreaks = NSwitch(self, height=iPx)
+ self.addRow(self._build.getLabel("md.preserveBreaks"), self.mdPreserveBreaks)
+
# Finalise
self.finalise()
@@ -1260,6 +1266,7 @@ class _OutputTab(NScrollableForm):
self.odtPageCountOffset.setValue(self._build.getInt("odt.pageCountOffset"))
self.htmlAddStyles.setChecked(self._build.getBool("html.addStyles"))
self.htmlPreserveTabs.setChecked(self._build.getBool("html.preserveTabs"))
+ self.mdPreserveBreaks.setChecked(self._build.getBool("md.preserveBreaks"))
return
def saveContent(self) -> None:
@@ -1269,6 +1276,7 @@ class _OutputTab(NScrollableForm):
self._build.setValue("odt.pageCountOffset", self.odtPageCountOffset.value())
self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked())
self._build.setValue("html.preserveTabs", self.htmlPreserveTabs.isChecked())
+ self._build.setValue("md.preserveBreaks", self.mdPreserveBreaks.isChecked())
return
##
From 44adc42fa6043055bf0c43fd719743fc31384a57 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 1 Mar 2024 22:15:28 +0100
Subject: [PATCH 3/9] Make sure manuscript preview always preserves tabs
---
novelwriter/core/docbuild.py | 24 ++++++++++++++++++++----
novelwriter/tools/manuscript.py | 3 ++-
tests/test_core/test_core_docbuild.py | 3 ++-
3 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index 924a038a..d3e10987 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -52,15 +52,16 @@ class NWBuildDocument:
manuscript, based on a build definition object (BuildSettings).
"""
- __slots__ = ("_project", "_build", "_queue", "_error", "_cache", "_count")
+ __slots__ = ("_project", "_build", "_queue", "_error", "_cache", "_count", "_preview")
- def __init__(self, project: NWProject, build: BuildSettings, doCount: bool = False) -> None:
+ def __init__(self, project: NWProject, build: BuildSettings) -> None:
self._project = project
self._build = build
self._queue = []
self._error = None
self._cache = None
- self._count = doCount
+ self._count = False
+ self._preview = False
return
##
@@ -80,6 +81,21 @@ class NWBuildDocument:
"""
return self._cache
+ ##
+ # Setters
+ ##
+
+ def setCountEnabled(self, state: bool) -> None:
+ """Turn on/off stats counting for builds."""
+ self._count = state
+ return
+
+ def setPreviewMode(self, state: bool) -> None:
+ """Set the preview mode of the build. Implies count mode."""
+ self._preview = state
+ self._count = state
+ return
+
##
# Special Methods
##
@@ -161,7 +177,7 @@ class NWBuildDocument:
else:
yield i, False
- if not self._build.getBool("html.preserveTabs"):
+ if not (self._build.getBool("html.preserveTabs") or self._preview):
makeObj.replaceTabs()
self._error = None
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 65926c1d..393b0e0a 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -337,7 +337,8 @@ class GuiManuscript(QDialog):
if build is None:
return
- docBuild = NWBuildDocument(SHARED.project, build, doCount=True)
+ docBuild = NWBuildDocument(SHARED.project, build)
+ docBuild.setPreviewMode(True)
docBuild.queueAll()
self.docPreview.beginNewBuild(len(docBuild))
diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py
index 991b378f..9a10ec75 100644
--- a/tests/test_core/test_core_docbuild.py
+++ b/tests/test_core/test_core_docbuild.py
@@ -83,7 +83,8 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
build = BuildSettings()
build.unpack(BUILD_CONF)
- docBuild = NWBuildDocument(project, build, doCount=True)
+ docBuild = NWBuildDocument(project, build)
+ docBuild.setCountEnabled(True)
docBuild.queueAll()
assert len(docBuild) == 21
From 3e3335be39b7cc7d706793ba5b5d97d12ab0f797 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 2 Mar 2024 00:19:02 +0100
Subject: [PATCH 4/9] Add additional styling for headers
---
novelwriter/core/buildsettings.py | 6 ++
novelwriter/core/docbuild.py | 12 ++++
novelwriter/core/tokenizer.py | 66 ++++++++++++------
novelwriter/tools/manussettings.py | 104 ++++++++++++++++++++++++-----
4 files changed, 153 insertions(+), 35 deletions(-)
diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py
index 1037c572..44f186cb 100644
--- a/novelwriter/core/buildsettings.py
+++ b/novelwriter/core/buildsettings.py
@@ -59,6 +59,12 @@ SETTINGS_TEMPLATE = {
"headings.fmtSection": (str, ""),
"headings.hideScene": (bool, False),
"headings.hideSection": (bool, True),
+ "headings.centerTitle": (bool, True),
+ "headings.centerChapter": (bool, False),
+ "headings.centerScene": (bool, False),
+ "headings.breakTitle": (bool, True),
+ "headings.breakChapter": (bool, True),
+ "headings.breakScene": (bool, False),
"text.includeSynopsis": (bool, False),
"text.includeComments": (bool, False),
"text.includeKeywords": (bool, False),
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index d3e10987..a7966074 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -286,6 +286,18 @@ class NWBuildDocument:
self._build.getStr("headings.fmtSection"),
self._build.getBool("headings.hideSection")
)
+ bldObj.setTitleStyle(
+ self._build.getBool("headings.centerTitle"),
+ self._build.getBool("headings.breakTitle")
+ )
+ bldObj.setChapterStyle(
+ self._build.getBool("headings.centerChapter"),
+ self._build.getBool("headings.breakChapter")
+ )
+ bldObj.setSceneStyle(
+ self._build.getBool("headings.centerScene"),
+ self._build.getBool("headings.breakScene")
+ )
bldObj.setFont(fontFamily, textSize, textFixed)
bldObj.setJustify(self._build.getBool("format.justifyText"))
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index 1bd3d794..f148236f 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -162,6 +162,10 @@ class Tokenizer(ABC):
self._linkHeaders = False # Add an anchor before headers
+ self._titleStyle = self.A_CENTRE | self.A_PBB
+ self._chapterStyle = self.A_PBB
+ self._sceneStyle = self.A_NONE
+
# Instance Variables
self._hFormatter = HeadingFormatter(self._project)
self._skipSeparator = False # Flag to indicate that we skip the scene separator
@@ -171,6 +175,7 @@ class Tokenizer(ABC):
self._isNovel = False # Document is a novel document
self._isNote = False # Document is a project note
self._isFirst = True # Document is the first in a set
+ self._noBreak = False # Don't allow an initial break when formatting headers
# Error Handling
self._errData = []
@@ -255,6 +260,27 @@ class Tokenizer(ABC):
self._hideSection = hide
return
+ def setTitleStyle(self, center: bool, pageBreak: bool) -> None:
+ """Set the title heading style."""
+ self._titleStyle = (
+ (self.A_CENTRE if center else self.A_NONE) | (self.A_PBB if pageBreak else self.A_NONE)
+ )
+ return
+
+ def setChapterStyle(self, center: bool, pageBreak: bool) -> None:
+ """Set the chapter heading style."""
+ self._chapterStyle = (
+ (self.A_CENTRE if center else self.A_NONE) | (self.A_PBB if pageBreak else self.A_NONE)
+ )
+ return
+
+ def setSceneStyle(self, center: bool, pageBreak: bool) -> None:
+ """Set the scene heading style."""
+ self._sceneStyle = (
+ (self.A_CENTRE if center else self.A_NONE) | (self.A_PBB if pageBreak else self.A_NONE)
+ )
+ return
+
def setFont(self, family: str, size: int, isFixed: bool = False) -> None:
"""Set the build font."""
self._textFont = family
@@ -511,24 +537,17 @@ class Tokenizer(ABC):
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
- if self._isNovel:
- sAlign |= self.A_CENTRE
- sAlign |= self.A_PBB
-
nHead += 1
self._tokens.append((
- self.T_HEAD1, nHead, aLine[2:].strip(), [], sAlign
+ self.T_HEAD1, nHead, aLine[2:].strip(), [], self.A_NONE
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
- if self._isNovel:
- sAlign |= self.A_PBB
-
nHead += 1
self._tokens.append((
- self.T_HEAD2, nHead, aLine[3:].strip(), [], sAlign
+ self.T_HEAD2, nHead, aLine[3:].strip(), [], self.A_NONE
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -536,7 +555,7 @@ class Tokenizer(ABC):
elif aLine[:4] == "### ":
nHead += 1
self._tokens.append((
- self.T_HEAD3, nHead, aLine[4:].strip(), [], sAlign
+ self.T_HEAD3, nHead, aLine[4:].strip(), [], self.A_NONE
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -544,7 +563,7 @@ class Tokenizer(ABC):
elif aLine[:5] == "#### ":
nHead += 1
self._tokens.append((
- self.T_HEAD4, nHead, aLine[5:].strip(), [], sAlign
+ self.T_HEAD4, nHead, aLine[5:].strip(), [], self.A_NONE
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -557,7 +576,7 @@ class Tokenizer(ABC):
tStyle = self.T_HEAD1
self._tokens.append((
- tStyle, nHead, aLine[3:].strip(), [], sAlign | self.A_CENTRE
+ tStyle, nHead, aLine[3:].strip(), [], self.A_PBB | self.A_CENTRE
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -566,12 +585,11 @@ class Tokenizer(ABC):
nHead += 1
if self._isNovel:
tStyle = self.T_UNNUM
- sAlign |= self.A_PBB
else:
tStyle = self.T_HEAD2
self._tokens.append((
- tStyle, nHead, aLine[4:].strip(), [], sAlign
+ tStyle, nHead, aLine[4:].strip(), [], self.A_NONE
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -622,7 +640,8 @@ class Tokenizer(ABC):
# If we have content, turn off the first page flag
if self._isFirst and self._tokens:
- self._isFirst = False
+ self._isFirst = False # First document has been processed
+ self._noBreak = True # Checks again after headers are processed
# Make sure the token array doesn't start with a page break
# on the very first page, adding a blank first page.
@@ -686,7 +705,7 @@ class Tokenizer(ABC):
tTemp = self._hFormatter.apply(self._fmtTitle, token[2], token[1])
self._tokens[n] = (
- token[0], token[1], tTemp, [], token[4]
+ token[0], token[1], tTemp, [], self._titleStyle
)
# Set scene variables
@@ -704,7 +723,7 @@ class Tokenizer(ABC):
# Format the chapter header
self._tokens[n] = (
- token[0], token[1], tTemp, [], token[4]
+ token[0], token[1], tTemp, [], self._chapterStyle
)
# Set scene variables
@@ -729,11 +748,11 @@ class Tokenizer(ABC):
self._tokens[n] = (
self.T_EMPTY if self._skipSeparator else self.T_SEP, token[1],
"" if self._skipSeparator else tTemp, [],
- self.A_NONE if self._skipSeparator else (token[4] | self.A_CENTRE)
+ self.A_NONE if self._skipSeparator else self.A_CENTRE
)
else:
self._tokens[n] = (
- token[0], token[1], tTemp, [], token[4]
+ token[0], token[1], tTemp, [], self._sceneStyle
)
self._skipSeparator = False
@@ -758,6 +777,15 @@ class Tokenizer(ABC):
token[0], token[1], tTemp, [], token[4]
)
+ if n == 0 and self._noBreak:
+ # Make sure again that the token array doesn't start with a page break
+ self._noBreak = False
+ if self._tokens[0][4] & self.A_PBB:
+ token = self._tokens[0]
+ self._tokens[0] = (
+ token[0], token[1], token[2], token[3], token[4] & ~self.A_PBB
+ )
+
return True
def countStats(self) -> dict[str, int]:
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index 3ccf79e1..d27b8acf 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -593,6 +593,7 @@ class _HeadingsTab(NScrollablePage):
self._editing = 0
iPx = SHARED.theme.baseIconSize
+ sSp = CONFIG.pxInt(16)
vSp = CONFIG.pxInt(12)
bSp = CONFIG.pxInt(6)
@@ -614,8 +615,8 @@ class _HeadingsTab(NScrollablePage):
wrapTitle.addWidget(self.btnTitle)
wrapTitle.setSpacing(bSp)
- self.formatBox.addWidget(self.lblTitle, 0, 0, Qt.AlignLeft)
- self.formatBox.addLayout(wrapTitle, 0, 1, Qt.AlignLeft)
+ self.formatBox.addWidget(self.lblTitle, 0, 0)
+ self.formatBox.addLayout(wrapTitle, 0, 1)
# Chapter Heading
self.lblChapter = QLabel(self._build.getLabel("headings.fmtChapter"))
@@ -630,8 +631,8 @@ class _HeadingsTab(NScrollablePage):
wrapChapter.addWidget(self.btnChapter)
wrapChapter.setSpacing(bSp)
- self.formatBox.addWidget(self.lblChapter, 1, 0, Qt.AlignLeft)
- self.formatBox.addLayout(wrapChapter, 1, 1, Qt.AlignLeft)
+ self.formatBox.addWidget(self.lblChapter, 1, 0)
+ self.formatBox.addLayout(wrapChapter, 1, 1)
# Unnumbered Chapter Heading
self.lblUnnumbered = QLabel(self._build.getLabel("headings.fmtUnnumbered"))
@@ -646,8 +647,8 @@ class _HeadingsTab(NScrollablePage):
wrapUnnumbered.addWidget(self.btnUnnumbered)
wrapUnnumbered.setSpacing(bSp)
- self.formatBox.addWidget(self.lblUnnumbered, 2, 0, Qt.AlignLeft)
- self.formatBox.addLayout(wrapUnnumbered, 2, 1, Qt.AlignLeft)
+ self.formatBox.addWidget(self.lblUnnumbered, 2, 0)
+ self.formatBox.addLayout(wrapUnnumbered, 2, 1)
# Scene Heading
sceneHideTip = self._build.getLabel("headings.hideScene")
@@ -672,9 +673,9 @@ class _HeadingsTab(NScrollablePage):
wrapSceneHide.addWidget(self.swtScene)
wrapSceneHide.setSpacing(bSp)
- self.formatBox.addWidget(self.lblScene, 3, 0, Qt.AlignLeft)
- self.formatBox.addLayout(wrapScene, 3, 1, Qt.AlignLeft)
- self.formatBox.addLayout(wrapSceneHide, 3, 2, Qt.AlignLeft)
+ self.formatBox.addWidget(self.lblScene, 3, 0)
+ self.formatBox.addLayout(wrapScene, 3, 1)
+ self.formatBox.addLayout(wrapSceneHide, 3, 2)
# Section Heading
sectionHideTip = self._build.getLabel("headings.hideSection")
@@ -699,16 +700,16 @@ class _HeadingsTab(NScrollablePage):
wrapSectionHide.addWidget(self.swtSection)
wrapSectionHide.setSpacing(bSp)
- self.formatBox.addWidget(self.lblSection, 4, 0, Qt.AlignLeft)
- self.formatBox.addLayout(wrapSection, 4, 1, Qt.AlignLeft)
- self.formatBox.addLayout(wrapSectionHide, 4, 2, Qt.AlignLeft)
+ self.formatBox.addWidget(self.lblSection, 4, 0)
+ self.formatBox.addLayout(wrapSection, 4, 1)
+ self.formatBox.addLayout(wrapSectionHide, 4, 2)
# Edit Form
# =========
self.lblEditForm = QLabel(self.tr("Editing: {0}").format(self.tr("None")))
- self.editTextBox = QPlainTextEdit()
+ self.editTextBox = QPlainTextEdit(self)
self.editTextBox.setFixedHeight(5*iPx)
self.editTextBox.setEnabled(False)
@@ -735,10 +736,10 @@ class _HeadingsTab(NScrollablePage):
self.aInsCharPOV.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.CHAR_POV))
self.aInsCharFocus.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.CHAR_FOCUS))
- self.btnInsert = QPushButton(self.tr("Insert"))
+ self.btnInsert = QPushButton(self.tr("Insert"), self)
self.btnInsert.setMenu(self.menuInsert)
- self.btnApply = QPushButton(self.tr("Apply"))
+ self.btnApply = QPushButton(self.tr("Apply"), self)
self.btnApply.clicked.connect(self._saveFormat)
self.formButtonBox = QHBoxLayout()
@@ -751,13 +752,72 @@ class _HeadingsTab(NScrollablePage):
self.editFormBox.addWidget(self.editTextBox)
self.editFormBox.addLayout(self.formButtonBox)
+ # Layout Matrix
+ # =============
+ self.layoutMatrix = QGridLayout()
+ self.layoutMatrix.setVerticalSpacing(vSp)
+ self.layoutMatrix.setHorizontalSpacing(vSp)
+
+ # Heading
+ self.layoutHeading = QLabel("{0}".format(self.tr("Additional Styling")), self)
+ self.layoutMatrix.addWidget(self.layoutHeading, 0, 0, 1, 5)
+
+ # Title Layout
+ self.mtxTitle = QLabel(self._build.getLabel("headings.fmtTitle"))
+ self.centerTitle = NSwitch(self, height=iPx)
+ self.breakTitle = NSwitch(self, height=iPx)
+ lblCenterT = QLabel(self.tr("Centre"))
+ lblCenterT.setIndent(sSp)
+ lblBreakT = QLabel(self.tr("Page Break"))
+ lblBreakT.setIndent(sSp)
+
+ self.layoutMatrix.addWidget(self.mtxTitle, 1, 0)
+ self.layoutMatrix.addWidget(lblCenterT, 1, 1)
+ self.layoutMatrix.addWidget(self.centerTitle, 1, 2)
+ self.layoutMatrix.addWidget(lblBreakT, 1, 3)
+ self.layoutMatrix.addWidget(self.breakTitle, 1, 4)
+
+ # Chapter Layout
+ self.mtxChapter = QLabel(self._build.getLabel("headings.fmtChapter"))
+ self.centerChapter = NSwitch(self, height=iPx)
+ self.breakChapter = NSwitch(self, height=iPx)
+ lblCenterC = QLabel(self.tr("Centre"))
+ lblCenterC.setIndent(sSp)
+ lblBreakC = QLabel(self.tr("Page Break"))
+ lblBreakC.setIndent(sSp)
+
+ self.layoutMatrix.addWidget(self.mtxChapter, 2, 0)
+ self.layoutMatrix.addWidget(lblCenterC, 2, 1)
+ self.layoutMatrix.addWidget(self.centerChapter, 2, 2)
+ self.layoutMatrix.addWidget(lblBreakC, 2, 3)
+ self.layoutMatrix.addWidget(self.breakChapter, 2, 4)
+
+ # Scene Layout
+ self.mtxScene = QLabel(self._build.getLabel("headings.fmtScene"))
+ self.centerScene = NSwitch(self, height=iPx)
+ self.breakScene = NSwitch(self, height=iPx)
+ lblCenterS = QLabel(self.tr("Centre"))
+ lblCenterS.setIndent(sSp)
+ lblBreakS = QLabel(self.tr("Page Break"))
+ lblBreakS.setIndent(sSp)
+
+ self.layoutMatrix.addWidget(self.mtxScene, 3, 0)
+ self.layoutMatrix.addWidget(lblCenterS, 3, 1)
+ self.layoutMatrix.addWidget(self.centerScene, 3, 2)
+ self.layoutMatrix.addWidget(lblBreakS, 3, 3)
+ self.layoutMatrix.addWidget(self.breakScene, 3, 4)
+
+ self.layoutMatrix.setColumnStretch(5, 1)
+
# Assemble
# ========
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.formatBox)
- self.outerBox.addSpacing(CONFIG.pxInt(16))
+ self.outerBox.addSpacing(sSp)
self.outerBox.addLayout(self.editFormBox)
+ self.outerBox.addSpacing(sSp)
+ self.outerBox.addLayout(self.layoutMatrix)
self.outerBox.addStretch(1)
self.setCentralLayout(self.outerBox)
@@ -773,12 +833,24 @@ class _HeadingsTab(NScrollablePage):
self.fmtSection.setText(self._build.getStr("headings.fmtSection"))
self.swtScene.setChecked(self._build.getBool("headings.hideScene"))
self.swtSection.setChecked(self._build.getBool("headings.hideSection"))
+ self.centerTitle.setChecked(self._build.getBool("headings.centerTitle"))
+ self.centerChapter.setChecked(self._build.getBool("headings.centerChapter"))
+ self.centerScene.setChecked(self._build.getBool("headings.centerScene"))
+ self.breakTitle.setChecked(self._build.getBool("headings.breakTitle"))
+ self.breakChapter.setChecked(self._build.getBool("headings.breakChapter"))
+ self.breakScene.setChecked(self._build.getBool("headings.breakScene"))
return
def saveContent(self) -> None:
"""Save choices back into build object."""
self._build.setValue("headings.hideScene", self.swtScene.isChecked())
self._build.setValue("headings.hideSection", self.swtSection.isChecked())
+ self._build.setValue("headings.centerTitle", self.centerTitle.isChecked())
+ self._build.setValue("headings.centerChapter", self.centerChapter.isChecked())
+ self._build.setValue("headings.centerScene", self.centerScene.isChecked())
+ self._build.setValue("headings.breakTitle", self.breakTitle.isChecked())
+ self._build.setValue("headings.breakChapter", self.breakChapter.isChecked())
+ self._build.setValue("headings.breakScene", self.breakScene.isChecked())
return
##
From 82349d39b741d436627ce290ff56d7eaaefa7dc8 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 2 Mar 2024 00:19:21 +0100
Subject: [PATCH 5/9] Fix broken tests
---
tests/test_core/test_core_tohtml.py | 24 +++++++++++++++++++--
tests/test_core/test_core_tokenizer.py | 30 ++++++++++++++++++++++++--
tests/test_core/test_core_toodt.py | 10 +++++++++
3 files changed, 60 insertions(+), 4 deletions(-)
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index a274f2a6..80ac1b65 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -44,6 +44,7 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
# Header 1
html._text = "# Partition\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == (
"
Partition
\n"
@@ -52,6 +53,7 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
# Header 2
html._text = "## Chapter Title\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == (
"Chapter Title
\n"
@@ -60,26 +62,30 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
# Header 3
html._text = "### Scene Title\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == "Scene Title
\n"
# Header 4
html._text = "#### Section Title\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == "Section Title
\n"
# Title
html._text = "#! Title\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == (
- "Title
\n"
+ "Title
\n"
)
# Unnumbered
html._text = "##! Prologue\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == "Prologue
\n"
@@ -94,38 +100,45 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
# Header 1
html._text = "# Heading One\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == "Heading One
\n"
# Header 2
html._text = "## Heading Two\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == "Heading Two
\n"
# Header 3
html._text = "### Heading Three\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == "Heading Three
\n"
# Header 4
html._text = "#### Heading Four\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == "Heading Four
\n"
# Title
html._text = "#! Heading One\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == (
- "Heading One
\n"
+ ""
+ "Heading One
\n"
)
# Unnumbered
html._text = "##! Heading Two\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == "Heading Two
\n"
@@ -227,9 +240,12 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
)
# Multiple Keywords
+ html._isFirst = False
+ html._noBreak = False
html.setKeywords(True)
html._text = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == (
"Chapter
\n"
@@ -457,8 +473,11 @@ def testCoreToHtml_SpecialCases(mockGUI):
"\n"
)
+ html._isFirst = False
+ html._noBreak = False
html._text = "## Heading <1>\n"
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == (
"Heading <1>
\n"
@@ -532,6 +551,7 @@ def testCoreToHtml_Complex(mockGUI, fncPath):
html._text = docText[i]
html.doPreProcessing()
html.tokenizeText()
+ html.doHeaders()
html.doConvert()
assert html.result == resText[i]
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index f722c89f..ea744eae 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -276,6 +276,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens._text = "# Novel Title\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Novel Title", [], Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -289,6 +290,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens._text = "# Note Title\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Note Title", [], Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -301,9 +303,11 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
tokens._isNote = False
+ tokens._noBreak = False
tokens._text = "## Chapter One\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD2, 1, "Chapter One", [], Tokenizer.A_PBB),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -316,6 +320,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens._text = "## Heading 2\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD2, 1, "Heading 2", [], Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -331,6 +336,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens._text = "### Scene One\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD3, 1, "Scene One", [], Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -343,6 +349,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens._text = "### Heading 3\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD3, 1, "Heading 3", [], Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -358,6 +365,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens._text = "#### A Section\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD4, 1, "A Section", [], Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -370,6 +378,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens._text = "#### Heading 4\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD4, 1, "Heading 4", [], Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -382,11 +391,13 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
tokens._isNote = False
+ tokens._isFirst = False
tokens._text = "#! Title\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
- (Tokenizer.T_TITLE, 1, "Title", [], Tokenizer.A_CENTRE),
+ (Tokenizer.T_TITLE, 1, "Title", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
]
assert tokens.allMarkdown[-1] == "#! Title\n\n"
@@ -394,11 +405,13 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File
tokens._isNovel = False
tokens._isNote = True
+ tokens._isFirst = False
tokens._text = "#! Title\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
- (Tokenizer.T_HEAD1, 1, "Title", [], Tokenizer.A_CENTRE),
+ (Tokenizer.T_HEAD1, 1, "Title", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
]
assert tokens.allMarkdown[-1] == "#! Title\n\n"
@@ -412,6 +425,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens._text = "##! Prologue\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_UNNUM, 1, "Prologue", [], Tokenizer.A_PBB),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -424,6 +438,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens._text = "##! Prologue\n"
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD2, 1, "Prologue", [], Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -822,6 +837,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"# Title Two\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == correctResp
# Command w/Space
@@ -832,6 +848,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"# Title Two\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == correctResp
# Trailing Spaces
@@ -842,6 +859,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"# Title Two\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == correctResp
# Single Empty Paragraph
@@ -853,6 +871,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"Some text to go here ...\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -873,6 +892,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"Some text to go here ...\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -890,6 +910,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"Some text to go here ...\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -909,6 +930,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"Some text to go here ...\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -925,6 +947,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"Some text to go here ...\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -941,6 +964,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"Some text to go here ...\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -961,6 +985,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"Some text to go here ...\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
@@ -980,6 +1005,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"Some text to go here ...\n\n"
)
tokens.tokenizeText()
+ tokens.doHeaders()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index 23afa0ac..ccfb3cce 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -246,6 +246,7 @@ def testCoreToOdt_ConvertHeaders(mockGUI):
# Header 1
odt._text = "# Title\n"
odt.tokenizeText()
+ odt.doHeaders()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
@@ -259,6 +260,7 @@ def testCoreToOdt_ConvertHeaders(mockGUI):
# Header 2
odt._text = "## Chapter\n"
odt.tokenizeText()
+ odt.doHeaders()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
@@ -272,6 +274,7 @@ def testCoreToOdt_ConvertHeaders(mockGUI):
# Header 3
odt._text = "### Scene\n"
odt.tokenizeText()
+ odt.doHeaders()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
@@ -285,6 +288,7 @@ def testCoreToOdt_ConvertHeaders(mockGUI):
# Header 4
odt._text = "#### Section\n"
odt.tokenizeText()
+ odt.doHeaders()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
@@ -296,8 +300,10 @@ def testCoreToOdt_ConvertHeaders(mockGUI):
)
# Title
+ odt._isFirst = True
odt._text = "#! Title\n"
odt.tokenizeText()
+ odt.doHeaders()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
@@ -311,6 +317,7 @@ def testCoreToOdt_ConvertHeaders(mockGUI):
# Unnumbered chapter
odt._text = "##! Prologue\n"
odt.tokenizeText()
+ odt.doHeaders()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
@@ -605,6 +612,7 @@ def testCoreToOdt_ConvertParagraphs(mockGUI):
"Text\n\n"
)
odt.tokenizeText()
+ odt.doHeaders()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
@@ -729,6 +737,7 @@ def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
"Text\n\n"
)
odt.tokenizeText()
+ odt.doHeaders()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
@@ -768,6 +777,7 @@ def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths):
"Text\n\n"
)
odt.tokenizeText()
+ odt.doHeaders()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
From c6cbe4cc0d18331e6cf3ac456fd7379e436d8510 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 2 Mar 2024 00:32:42 +0100
Subject: [PATCH 6/9] Allow titles in notes as well
---
novelwriter/core/tokenizer.py | 16 ++++------------
tests/test_core/test_core_tohtml.py | 2 +-
tests/test_core/test_core_tokenizer.py | 4 ++--
3 files changed, 7 insertions(+), 15 deletions(-)
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index f148236f..ef3136fd 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -570,24 +570,16 @@ class Tokenizer(ABC):
elif aLine[:3] == "#! ":
nHead += 1
- if self._isNovel:
- tStyle = self.T_TITLE
- else:
- tStyle = self.T_HEAD1
-
self._tokens.append((
- tStyle, nHead, aLine[3:].strip(), [], self.A_PBB | self.A_CENTRE
+ self.T_TITLE, nHead, aLine[3:].strip(), [], self.A_PBB | self.A_CENTRE
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "##! ":
nHead += 1
- if self._isNovel:
- tStyle = self.T_UNNUM
- else:
- tStyle = self.T_HEAD2
-
+ # If we're not in a novel section, we just treat this as a regular H2
+ tStyle = self.T_UNNUM if self._isNovel else self.T_HEAD2
self._tokens.append((
tStyle, nHead, aLine[4:].strip(), [], self.A_NONE
))
@@ -641,7 +633,7 @@ class Tokenizer(ABC):
# If we have content, turn off the first page flag
if self._isFirst and self._tokens:
self._isFirst = False # First document has been processed
- self._noBreak = True # Checks again after headers are processed
+ self._noBreak = True # Check again after headers are processed
# Make sure the token array doesn't start with a page break
# on the very first page, adding a blank first page.
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index 80ac1b65..3ccfecf3 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -131,7 +131,7 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
html.doHeaders()
html.doConvert()
assert html.result == (
- ""
+ ""
"Heading One
\n"
)
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index ea744eae..2538d760 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -261,7 +261,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens.tokenizeText()
assert tokens._tokens == [
- (Tokenizer.T_HEAD1, 1, "Note Title", [], Tokenizer.A_CENTRE),
+ (Tokenizer.T_TITLE, 1, "Note Title", [], Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
]
assert tokens.allMarkdown[-1] == "#! Note Title\n\n"
@@ -411,7 +411,7 @@ def testCoreToken_HeaderFormat(mockGUI):
tokens.tokenizeText()
tokens.doHeaders()
assert tokens._tokens == [
- (Tokenizer.T_HEAD1, 1, "Title", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
+ (Tokenizer.T_TITLE, 1, "Title", [], Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
]
assert tokens.allMarkdown[-1] == "#! Title\n\n"
From 415d46d6590574537fcb1e5089412058460f95b7 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 2 Mar 2024 14:46:10 +0100
Subject: [PATCH 7/9] Add a specific header styles test
---
tests/test_core/test_core_tokenizer.py | 274 +++++++++++++++++++++++++
1 file changed, 274 insertions(+)
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index 2538d760..d985306d 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -448,6 +448,280 @@ def testCoreToken_HeaderFormat(mockGUI):
# END Test testCoreToken_HeaderFormat
+@pytest.mark.core
+def testCoreToken_HeaderStyle(mockGUI):
+ """Test the styling of headers in the Tokenizer class."""
+ project = NWProject()
+ tokens = BareTokenizer(project)
+
+ def processStyle(text: str, first: bool) -> int:
+ tokens._text = text
+ tokens._isFirst = first
+ tokens._noBreak = first
+ tokens.tokenizeText()
+ tokens.doHeaders()
+ return tokens._tokens[0][4]
+
+ # No Styles
+ # =========
+
+ tokens.setTitleStyle(False, False)
+ tokens.setChapterStyle(False, False)
+ tokens.setSceneStyle(False, False)
+
+ assert tokens._titleStyle == Tokenizer.A_NONE
+ assert tokens._chapterStyle == Tokenizer.A_NONE
+ assert tokens._sceneStyle == Tokenizer.A_NONE
+
+ # Novel Docs
+ tokens._isNovel = True
+ tokens._isNote = False
+
+ # First Document is False
+ assert processStyle("# Title\n", False) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", False) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_NONE
+
+ # First Document is True
+ assert processStyle("# Title\n", True) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", True) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", True) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", True) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("##! Prologue\n", True) == Tokenizer.A_NONE
+
+ # Note Docs
+ tokens._isNovel = False
+ tokens._isNote = True
+
+ # First Document is False
+ assert processStyle("# Title\n", False) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", False) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_NONE
+
+ # First Document is True
+ assert processStyle("# Title\n", True) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", True) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", True) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", True) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("##! Prologue\n", True) == Tokenizer.A_NONE
+
+ # Center Headers
+ # ==============
+
+ tokens.setTitleStyle(True, False)
+ tokens.setChapterStyle(True, False)
+ tokens.setSceneStyle(True, False)
+
+ assert tokens._titleStyle == Tokenizer.A_CENTRE
+ assert tokens._chapterStyle == Tokenizer.A_CENTRE
+ assert tokens._sceneStyle == Tokenizer.A_CENTRE
+
+ # Novel Docs
+ tokens._isNovel = True
+ tokens._isNote = False
+
+ # First Document is False
+ assert processStyle("# Title\n", False) == Tokenizer.A_CENTRE
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_CENTRE
+ assert processStyle("### Scene\n", False) == Tokenizer.A_CENTRE
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_CENTRE
+
+ # First Document is True
+ assert processStyle("# Title\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("## Chapter\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("### Scene\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("#### Section\n", True) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("##! Prologue\n", True) == Tokenizer.A_CENTRE
+
+ # Note Docs
+ tokens._isNovel = False
+ tokens._isNote = True
+
+ # First Document is False
+ assert processStyle("# Title\n", False) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", False) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_NONE
+
+ # First Document is True
+ assert processStyle("# Title\n", True) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", True) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", True) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", True) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("##! Prologue\n", True) == Tokenizer.A_NONE
+
+ # Page Break Headers
+ # ==================
+
+ tokens.setTitleStyle(False, True)
+ tokens.setChapterStyle(False, True)
+ tokens.setSceneStyle(False, True)
+
+ assert tokens._titleStyle == Tokenizer.A_PBB
+ assert tokens._chapterStyle == Tokenizer.A_PBB
+ assert tokens._sceneStyle == Tokenizer.A_PBB
+
+ # Novel Docs
+ tokens._isNovel = True
+ tokens._isNote = False
+
+ # First Document is False
+ assert processStyle("# Title\n", False) == Tokenizer.A_PBB
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_PBB
+ assert processStyle("### Scene\n", False) == Tokenizer.A_PBB
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_PBB
+
+ # First Document is True
+ assert processStyle("# Title\n", True) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", True) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", True) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", True) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("##! Prologue\n", True) == Tokenizer.A_NONE
+
+ # Note Docs
+ tokens._isNovel = False
+ tokens._isNote = True
+
+ # First Document is False
+ assert processStyle("# Title\n", False) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", False) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_NONE
+
+ # First Document is True
+ assert processStyle("# Title\n", True) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", True) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", True) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", True) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("##! Prologue\n", True) == Tokenizer.A_NONE
+
+ # Page Break and Centre Headers
+ # =============================
+
+ tokens.setTitleStyle(True, True)
+ tokens.setChapterStyle(True, True)
+ tokens.setSceneStyle(True, True)
+
+ assert tokens._titleStyle == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert tokens._chapterStyle == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert tokens._sceneStyle == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+
+ # Novel Docs
+ tokens._isNovel = True
+ tokens._isNote = False
+
+ # First Document is False
+ assert processStyle("# Title\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("### Scene\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+
+ # First Document is True
+ assert processStyle("# Title\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("## Chapter\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("### Scene\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("#### Section\n", True) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("##! Prologue\n", True) == Tokenizer.A_CENTRE
+
+ # Note Docs
+ tokens._isNovel = False
+ tokens._isNote = True
+
+ # First Document is False
+ assert processStyle("# Title\n", False) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", False) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_NONE
+
+ # First Document is True
+ assert processStyle("# Title\n", True) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", True) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", True) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", True) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", True) == Tokenizer.A_CENTRE
+ assert processStyle("##! Prologue\n", True) == Tokenizer.A_NONE
+
+ # Check Separation
+ # ================
+ tokens._isNovel = True
+ tokens._isNote = False
+
+ # Title Styles
+ tokens.setTitleStyle(True, True)
+ tokens.setChapterStyle(False, False)
+ tokens.setSceneStyle(False, False)
+
+ assert tokens._titleStyle == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert tokens._chapterStyle == Tokenizer.A_NONE
+ assert tokens._sceneStyle == Tokenizer.A_NONE
+
+ assert processStyle("# Title\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", False) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_NONE
+
+ # Chapter Styles
+ tokens.setTitleStyle(False, False)
+ tokens.setChapterStyle(True, True)
+ tokens.setSceneStyle(False, False)
+
+ assert tokens._titleStyle == Tokenizer.A_NONE
+ assert tokens._chapterStyle == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert tokens._sceneStyle == Tokenizer.A_NONE
+
+ assert processStyle("# Title\n", False) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("### Scene\n", False) == Tokenizer.A_NONE
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+
+ # Scene Styles
+ tokens.setTitleStyle(False, False)
+ tokens.setChapterStyle(False, False)
+ tokens.setSceneStyle(True, True)
+
+ assert tokens._titleStyle == Tokenizer.A_NONE
+ assert tokens._chapterStyle == Tokenizer.A_NONE
+ assert tokens._sceneStyle == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+
+ assert processStyle("# Title\n", False) == Tokenizer.A_NONE
+ assert processStyle("## Chapter\n", False) == Tokenizer.A_NONE
+ assert processStyle("### Scene\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("#### Section\n", False) == Tokenizer.A_NONE
+ assert processStyle("#! My Novel\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ assert processStyle("##! Prologue\n", False) == Tokenizer.A_NONE
+
+# END Test testCoreToken_HeaderStyle
+
+
@pytest.mark.core
def testCoreToken_MetaFormat(mockGUI):
"""Test the tokenization of meta formats in the Tokenizer class."""
From aea41922132fe5a4eafafc3e6c21ee09fc11cd27 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 2 Mar 2024 15:24:07 +0100
Subject: [PATCH 8/9] Add option to ignore certain keywords in manuscript
(#1132)
---
novelwriter/core/buildsettings.py | 2 ++
novelwriter/core/docbuild.py | 3 ++-
novelwriter/core/tokenizer.py | 38 +++++++++++++++-----------
novelwriter/tools/manussettings.py | 43 ++++++++++++++++++++++++++----
4 files changed, 65 insertions(+), 21 deletions(-)
diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py
index 44f186cb..8557a523 100644
--- a/novelwriter/core/buildsettings.py
+++ b/novelwriter/core/buildsettings.py
@@ -69,6 +69,7 @@ SETTINGS_TEMPLATE = {
"text.includeComments": (bool, False),
"text.includeKeywords": (bool, False),
"text.includeBodyText": (bool, True),
+ "text.ignoredKeywords": (str, ""),
"text.addNoteHeadings": (bool, True),
"format.textFont": (str, CONFIG.textFont),
"format.textSize": (int, 12),
@@ -112,6 +113,7 @@ SETTINGS_LABELS = {
"text.includeComments": QT_TRANSLATE_NOOP("Builds", "Include Comments"),
"text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
"text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
+ "text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
"text.grpInsert": QT_TRANSLATE_NOOP("Builds", "Insert Content"),
"text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"),
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index a7966074..e43816f8 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -303,10 +303,11 @@ class NWBuildDocument:
bldObj.setJustify(self._build.getBool("format.justifyText"))
bldObj.setLineHeight(self._build.getFloat("format.lineHeight"))
+ bldObj.setBodyText(self._build.getBool("text.includeBodyText"))
bldObj.setSynopsis(self._build.getBool("text.includeSynopsis"))
bldObj.setComments(self._build.getBool("text.includeComments"))
bldObj.setKeywords(self._build.getBool("text.includeKeywords"))
- bldObj.setBodyText(self._build.getBool("text.includeBodyText"))
+ bldObj.setIgnoredKeywords(self._build.getStr("text.ignoredKeywords"))
if isinstance(bldObj, ToHtml):
bldObj.setStyles(self._build.getBool("html.addStyles"))
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index ef3136fd..67871dfb 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -130,16 +130,17 @@ class Tokenizer(ABC):
self._tokens: list[tuple[int, int, str, list[tuple[int, int]], int]] = []
# User Settings
- self._textFont = "Serif" # Output text font
- self._textSize = 11 # Output text size
- self._textFixed = False # Fixed width text
- self._lineHeight = 1.15 # Line height in units of em
- self._blockIndent = 4.00 # Block indent in units of em
- self._doJustify = False # Justify text
- self._doBodyText = True # Include body text
- self._doSynopsis = False # Also process synopsis comments
- self._doComments = False # Also process comments
- self._doKeywords = False # Also process keywords like tags and references
+ self._textFont = "Serif" # Output text font
+ self._textSize = 11 # Output text size
+ self._textFixed = False # Fixed width text
+ self._lineHeight = 1.15 # Line height in units of em
+ self._blockIndent = 4.00 # Block indent in units of em
+ self._doJustify = False # Justify text
+ self._doBodyText = True # Include body text
+ self._doSynopsis = False # Also process synopsis comments
+ self._doComments = False # Also process comments
+ self._doKeywords = False # Also process keywords like tags and references
+ self._skipKeywords = set() # Keywords to ignore
# Margins
self._marginTitle = (1.000, 0.500)
@@ -363,6 +364,11 @@ class Tokenizer(ABC):
self._doKeywords = state
return
+ def setIgnoredKeywords(self, keywords: str) -> None:
+ """Comma separated string of keywords to ignore."""
+ self._skipKeywords = set(x.lower().strip() for x in keywords.split(","))
+ return
+
def setKeepMarkdown(self, state: bool) -> None:
"""Keep original markdown during build."""
self._keepMarkdown = state
@@ -530,11 +536,13 @@ class Tokenizer(ABC):
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
- self._tokens.append((
- self.T_KEYWORD, nHead, aLine[1:].strip(), [], sAlign
- ))
- if self._doKeywords and self._keepMarkdown:
- tmpMarkdown.append("%s\n" % aLine)
+ valid, bits, _ = self._project.index.scanThis(aLine)
+ if valid and bits and bits[0] not in self._skipKeywords:
+ self._tokens.append((
+ self.T_KEYWORD, nHead, aLine[1:].strip(), [], sAlign
+ ))
+ if self._doKeywords and self._keepMarkdown:
+ tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
nHead += 1
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index d27b8acf..14a3d529 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -37,7 +37,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
-from novelwriter.constants import nwHeadFmt, nwLabels, trConst
+from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, trConst
from novelwriter.core.buildsettings import BuildSettings, FilterMode
from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.modified import NComboBox, NDoubleSpinBox, NSpinBox
@@ -966,16 +966,32 @@ class _ContentTab(NScrollableForm):
iPx = SHARED.theme.baseIconSize
# Text Content
+ self.incBodyText = NSwitch(self, height=iPx)
self.incSynopsis = NSwitch(self, height=iPx)
self.incComments = NSwitch(self, height=iPx)
self.incKeywords = NSwitch(self, height=iPx)
- self.incBodyText = NSwitch(self, height=iPx)
+
+ self.ignoredKeywords = QLineEdit(self)
+
+ self.mnKeywords = QMenu(self)
+ for keyword in nwKeyWords.VALID_KEYS:
+ self.mnKeywords.addAction(
+ trConst(nwLabels.KEY_NAME[keyword]),
+ lambda keyword=keyword: self._updateIgnoredKeywords(keyword)
+ )
+
+ self.ignoredKeywordsButton = QToolButton(self)
+ self.ignoredKeywordsButton.setIcon(SHARED.theme.getIcon("add"))
+ self.ignoredKeywordsButton.setMenu(self.mnKeywords)
+ self.ignoredKeywordsButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
self.addGroupLabel(self._build.getLabel("text.grpContent"))
+ self.addRow(self._build.getLabel("text.includeBodyText"), self.incBodyText)
self.addRow(self._build.getLabel("text.includeSynopsis"), self.incSynopsis)
self.addRow(self._build.getLabel("text.includeComments"), self.incComments)
self.addRow(self._build.getLabel("text.includeKeywords"), self.incKeywords)
- self.addRow(self._build.getLabel("text.includeBodyText"), self.incBodyText)
+ self.addRow(self._build.getLabel("text.ignoredKeywords"), self.ignoredKeywords,
+ button=self.ignoredKeywordsButton, stretch=(1, 1))
# Insert Content
self.addNoteHead = NSwitch(self, height=iPx)
@@ -990,22 +1006,39 @@ class _ContentTab(NScrollableForm):
def loadContent(self) -> None:
"""Populate the widgets."""
+ self.incBodyText.setChecked(self._build.getBool("text.includeBodyText"))
self.incSynopsis.setChecked(self._build.getBool("text.includeSynopsis"))
self.incComments.setChecked(self._build.getBool("text.includeComments"))
self.incKeywords.setChecked(self._build.getBool("text.includeKeywords"))
- self.incBodyText.setChecked(self._build.getBool("text.includeBodyText"))
+ self.ignoredKeywords.setText(self._build.getStr("text.ignoredKeywords"))
self.addNoteHead.setChecked(self._build.getBool("text.addNoteHeadings"))
+ self._updateIgnoredKeywords()
return
def saveContent(self) -> None:
"""Save choices back into build object."""
+ self._updateIgnoredKeywords()
+ self._build.setValue("text.includeBodyText", self.incBodyText.isChecked())
self._build.setValue("text.includeSynopsis", self.incSynopsis.isChecked())
self._build.setValue("text.includeComments", self.incComments.isChecked())
self._build.setValue("text.includeKeywords", self.incKeywords.isChecked())
- self._build.setValue("text.includeBodyText", self.incBodyText.isChecked())
+ self._build.setValue("text.ignoredKeywords", self.ignoredKeywords.text())
self._build.setValue("text.addNoteHeadings", self.addNoteHead.isChecked())
return
+ ##
+ # Internal Functions
+ ##
+
+ def _updateIgnoredKeywords(self, keyword: str | None = None) -> None:
+ """Update the ignored keywords list."""
+ current = [x.lower().strip() for x in self.ignoredKeywords.text().split(",")]
+ if keyword:
+ current.append(keyword)
+ verified = set(x for x in current if x in nwKeyWords.VALID_KEYS)
+ self.ignoredKeywords.setText(", ".join(verified))
+ return
+
# END Class _ContentTab
From bce4a164b855d384ed89cefc75b1d3074aa5b0df Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 2 Mar 2024 15:35:45 +0100
Subject: [PATCH 9/9] Add test coverage
---
tests/test_core/test_core_tokenizer.py | 9 +++++++++
tests/test_tools/test_tools_manussettings.py | 18 +++++++++++++-----
2 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index d985306d..e3d16863 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -809,6 +809,15 @@ def testCoreToken_MetaFormat(mockGUI):
]
assert tokens.allMarkdown[-1] == "@pov: Bod\n@plot: Main\n@location: Europe\n\n"
+ # Ignored keywords
+ tokens._text = "@pov: Bod\n@plot: Main\n@location: Europe\n"
+ tokens.setIgnoredKeywords("@plot, @location")
+ tokens.tokenizeText()
+ assert tokens._tokens == [
+ (Tokenizer.T_KEYWORD, 0, "pov: Bod", [], Tokenizer.A_NONE),
+ (Tokenizer.T_EMPTY, 0, "", [], Tokenizer.A_NONE),
+ ]
+
# END Test testCoreToken_MetaFormat
diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py
index 13fc49d6..45f97358 100644
--- a/tests/test_tools/test_tools_manussettings.py
+++ b/tests/test_tools/test_tools_manussettings.py
@@ -479,10 +479,11 @@ def testBuildSettings_Content(qtbot: QtBot, nwGUI: GuiMain):
"""Test the Content Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
+ build.setValue("text.includeBodyText", False)
build.setValue("text.includeSynopsis", False)
build.setValue("text.includeComments", False)
build.setValue("text.includeKeywords", False)
- build.setValue("text.includeBodyText", False)
+ build.setValue("text.ignoredKeywords", "")
build.setValue("text.addNoteHeadings", False)
@@ -496,28 +497,35 @@ def testBuildSettings_Content(qtbot: QtBot, nwGUI: GuiMain):
assert bSettings.toolStack.currentWidget() is contTab
# Check initial values
+ assert contTab.incBodyText.isChecked() is False
assert contTab.incSynopsis.isChecked() is False
assert contTab.incComments.isChecked() is False
assert contTab.incKeywords.isChecked() is False
- assert contTab.incBodyText.isChecked() is False
+ assert contTab.ignoredKeywords.text() == ""
assert contTab.addNoteHead.isChecked() is False
- # Toggle all
+ # Toggle switches
+ contTab.incBodyText.setChecked(True)
contTab.incSynopsis.setChecked(True)
contTab.incComments.setChecked(True)
contTab.incKeywords.setChecked(True)
- contTab.incBodyText.setChecked(True)
contTab.addNoteHead.setChecked(True)
+ # Test cleanup of ignored keywords
+ contTab.ignoredKeywords.setText("@stuff, @pizza, @object") # First two are invalid
+ contTab._updateIgnoredKeywords("@custom") # Adding a new should trigger cleanup
+ assert contTab.ignoredKeywords.text() in ("@custom, @object", "@object, @custom")
+
# Save values
contTab.saveContent()
+ assert build.getBool("text.includeBodyText") is True
assert build.getBool("text.includeSynopsis") is True
assert build.getBool("text.includeComments") is True
assert build.getBool("text.includeKeywords") is True
- assert build.getBool("text.includeBodyText") is True
+ assert build.getStr("text.ignoredKeywords") in ("@custom, @object", "@object, @custom")
assert build.getBool("text.addNoteHeadings") is True