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