diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index c2a04375..8557a523 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -59,10 +59,17 @@ 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), "text.includeBodyText": (bool, True), + "text.ignoredKeywords": (str, ""), "text.addNoteHeadings": (bool, True), "format.textFont": (str, CONFIG.textFont), "format.textSize": (int, 12), @@ -81,7 +88,9 @@ 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), } SETTINGS_LABELS = { @@ -104,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"), @@ -130,8 +140,12 @@ 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 8d6a0f21..e43816f8 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 self._build.getBool("format.replaceTabs"): + if not (self._build.getBool("html.preserveTabs") or self._preview): makeObj.replaceTabs() self._error = None @@ -192,6 +208,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]: @@ -268,15 +286,28 @@ 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")) 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 1bd3d794..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) @@ -162,6 +163,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 +176,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 +261,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 @@ -337,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 @@ -504,31 +536,26 @@ 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] == "# ": - 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 +563,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,34 +571,25 @@ 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) 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(), [], sAlign | 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 - sAlign |= self.A_PBB - 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(), [], 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 # 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. @@ -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/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/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/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index fe78f3b4..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 @@ -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 ## @@ -894,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) @@ -918,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 @@ -1245,6 +1350,15 @@ 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) + + # 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() @@ -1256,6 +1370,8 @@ 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")) + self.mdPreserveBreaks.setChecked(self._build.getBool("md.preserveBreaks")) return def saveContent(self) -> None: @@ -1264,6 +1380,8 @@ 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()) + self._build.setValue("md.preserveBreaks", self.mdPreserveBreaks.isChecked()) return ## 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 diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py index a274f2a6..3ccfecf3 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 == ( "