From 2018023dc7de0bf7c06e5971e17b8f2e6638aaf3 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 7 Nov 2024 14:34:35 +0100
Subject: [PATCH 1/6] Remove redundant if-checks
---
novelwriter/formats/todocx.py | 45 +++++++++++++++++------------------
novelwriter/formats/tohtml.py | 2 +-
novelwriter/formats/toodt.py | 41 ++++++++++++++++---------------
3 files changed, 43 insertions(+), 45 deletions(-)
diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py
index fe634b5e..5b499b45 100644
--- a/novelwriter/formats/todocx.py
+++ b/novelwriter/formats/todocx.py
@@ -236,32 +236,31 @@ class ToDocX(Tokenizer):
self._pars.append(par)
# Styles
- if tStyle is not None:
- if tStyle & BlockFmt.LEFT:
- par.setAlignment("left")
- elif tStyle & BlockFmt.RIGHT:
- par.setAlignment("right")
- elif tStyle & BlockFmt.CENTRE:
- par.setAlignment("center")
- elif tStyle & BlockFmt.JUSTIFY:
- par.setAlignment("both")
+ if tStyle & BlockFmt.LEFT:
+ par.setAlignment("left")
+ elif tStyle & BlockFmt.RIGHT:
+ par.setAlignment("right")
+ elif tStyle & BlockFmt.CENTRE:
+ par.setAlignment("center")
+ elif tStyle & BlockFmt.JUSTIFY:
+ par.setAlignment("both")
- if tStyle & BlockFmt.PBB:
- par.setPageBreakBefore(True)
- if tStyle & BlockFmt.PBA:
- par.setPageBreakAfter(True)
+ if tStyle & BlockFmt.PBB:
+ par.setPageBreakBefore(True)
+ if tStyle & BlockFmt.PBA:
+ par.setPageBreakAfter(True)
- if tStyle & BlockFmt.Z_BTM:
- par.setMarginBottom(0.0)
- if tStyle & BlockFmt.Z_TOP:
- par.setMarginTop(0.0)
+ if tStyle & BlockFmt.Z_BTM:
+ par.setMarginBottom(0.0)
+ if tStyle & BlockFmt.Z_TOP:
+ par.setMarginTop(0.0)
- if tStyle & BlockFmt.IND_T:
- par.setIndentFirst(True)
- if tStyle & BlockFmt.IND_L:
- par.setMarginLeft(bIndent)
- if tStyle & BlockFmt.IND_R:
- par.setMarginRight(bIndent)
+ if tStyle & BlockFmt.IND_T:
+ par.setIndentFirst(True)
+ if tStyle & BlockFmt.IND_L:
+ par.setMarginLeft(bIndent)
+ if tStyle & BlockFmt.IND_R:
+ par.setMarginRight(bIndent)
# Process Text Types
if tType == BlockTyp.TEXT:
diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py
index f3ef49d8..69a48ac2 100644
--- a/novelwriter/formats/tohtml.py
+++ b/novelwriter/formats/tohtml.py
@@ -171,7 +171,7 @@ class ToHtml(Tokenizer):
# Styles
aStyle = []
- if tStyle is not None and self._cssStyles:
+ if self._cssStyles:
if tStyle & BlockFmt.LEFT:
aStyle.append("text-align: left;")
elif tStyle & BlockFmt.RIGHT:
diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py
index f72aa6a4..af78f892 100644
--- a/novelwriter/formats/toodt.py
+++ b/novelwriter/formats/toodt.py
@@ -408,30 +408,29 @@ class ToOdt(Tokenizer):
# Styles
oStyle = ODTParagraphStyle("New")
- if tStyle is not None:
- if tStyle & BlockFmt.LEFT:
- oStyle.setTextAlign("left")
- elif tStyle & BlockFmt.RIGHT:
- oStyle.setTextAlign("right")
- elif tStyle & BlockFmt.CENTRE:
- oStyle.setTextAlign("center")
- elif tStyle & BlockFmt.JUSTIFY:
- oStyle.setTextAlign("justify")
+ if tStyle & BlockFmt.LEFT:
+ oStyle.setTextAlign("left")
+ elif tStyle & BlockFmt.RIGHT:
+ oStyle.setTextAlign("right")
+ elif tStyle & BlockFmt.CENTRE:
+ oStyle.setTextAlign("center")
+ elif tStyle & BlockFmt.JUSTIFY:
+ oStyle.setTextAlign("justify")
- if tStyle & BlockFmt.PBB:
- oStyle.setBreakBefore("page")
- if tStyle & BlockFmt.PBA:
- oStyle.setBreakAfter("page")
+ if tStyle & BlockFmt.PBB:
+ oStyle.setBreakBefore("page")
+ if tStyle & BlockFmt.PBA:
+ oStyle.setBreakAfter("page")
- if tStyle & BlockFmt.Z_BTM:
- oStyle.setMarginBottom("0.000cm")
- if tStyle & BlockFmt.Z_TOP:
- oStyle.setMarginTop("0.000cm")
+ if tStyle & BlockFmt.Z_BTM:
+ oStyle.setMarginBottom("0.000cm")
+ if tStyle & BlockFmt.Z_TOP:
+ oStyle.setMarginTop("0.000cm")
- if tStyle & BlockFmt.IND_L:
- oStyle.setMarginLeft(self._fBlockIndent)
- if tStyle & BlockFmt.IND_R:
- oStyle.setMarginRight(self._fBlockIndent)
+ if tStyle & BlockFmt.IND_L:
+ oStyle.setMarginLeft(self._fBlockIndent)
+ if tStyle & BlockFmt.IND_R:
+ oStyle.setMarginRight(self._fBlockIndent)
# Process Text Types
if tType == BlockTyp.TEXT:
From 0c659422b7240085a7bc8159cf5a934e6775298f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 7 Nov 2024 17:52:01 +0100
Subject: [PATCH 2/6] Add page break markers to the QTextDocument format class
---
novelwriter/formats/toqdoc.py | 82 ++++++++++++++++++++++++-----------
novelwriter/types.py | 9 +++-
2 files changed, 63 insertions(+), 28 deletions(-)
diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py
index de0a344c..bd9b3cb1 100644
--- a/novelwriter/formats/toqdoc.py
+++ b/novelwriter/formats/toqdoc.py
@@ -41,7 +41,7 @@ from novelwriter.formats.tokenizer import HEADINGS, Tokenizer
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakBefore,
- QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
+ QtPropLineHeight, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
)
logger = logging.getLogger(__name__)
@@ -75,6 +75,7 @@ class ToQTextDocument(Tokenizer):
self._init = False
self._bold = QFont.Weight.Bold
self._normal = QFont.Weight.Normal
+ self._newPage = False
self._pageSize = QPageSize(QPageSize.PageSizeId.A4)
self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
@@ -102,6 +103,11 @@ class ToQTextDocument(Tokenizer):
self._pageMargins = QMarginsF(left, top, right, bottom)
return
+ def setShowNewPage(self, state: bool) -> None:
+ """Add markers for page breaks."""
+ self._newPage = state
+ return
+
##
# Class Methods
##
@@ -149,8 +155,6 @@ class ToQTextDocument(Tokenizer):
# Text Formats
# ============
- QtPropLineHeight = QTextBlockFormat.LineHeightTypes.ProportionalHeight
-
self._blockFmt = QTextBlockFormat()
self._blockFmt.setTopMargin(self._mText[0])
self._blockFmt.setBottomMargin(self._mText[1])
@@ -184,32 +188,32 @@ class ToQTextDocument(Tokenizer):
bFmt.setTopMargin(self._mSep[0])
bFmt.setBottomMargin(self._mSep[1])
- if tStyle is not None:
- if tStyle & BlockFmt.LEFT:
- bFmt.setAlignment(QtAlignLeft)
- elif tStyle & BlockFmt.RIGHT:
- bFmt.setAlignment(QtAlignRight)
- elif tStyle & BlockFmt.CENTRE:
- bFmt.setAlignment(QtAlignCenter)
- elif tStyle & BlockFmt.JUSTIFY:
- bFmt.setAlignment(QtAlignJustify)
+ if tStyle & BlockFmt.LEFT:
+ bFmt.setAlignment(QtAlignLeft)
+ elif tStyle & BlockFmt.RIGHT:
+ bFmt.setAlignment(QtAlignRight)
+ elif tStyle & BlockFmt.CENTRE:
+ bFmt.setAlignment(QtAlignCenter)
+ elif tStyle & BlockFmt.JUSTIFY:
+ bFmt.setAlignment(QtAlignJustify)
- if tStyle & BlockFmt.PBB:
- bFmt.setPageBreakPolicy(QtPageBreakBefore)
- if tStyle & BlockFmt.PBA:
- bFmt.setPageBreakPolicy(QtPageBreakAfter)
+ if tStyle & BlockFmt.PBB:
+ self._insertNewPageMarker(cursor)
+ bFmt.setPageBreakPolicy(QtPageBreakBefore)
+ if tStyle & BlockFmt.PBA:
+ bFmt.setPageBreakPolicy(QtPageBreakAfter)
- if tStyle & BlockFmt.Z_BTM:
- bFmt.setBottomMargin(0.0)
- if tStyle & BlockFmt.Z_TOP:
- bFmt.setTopMargin(0.0)
+ if tStyle & BlockFmt.Z_BTM:
+ bFmt.setBottomMargin(0.0)
+ if tStyle & BlockFmt.Z_TOP:
+ bFmt.setTopMargin(0.0)
- if tStyle & BlockFmt.IND_L:
- bFmt.setLeftMargin(self._mIndent)
- if tStyle & BlockFmt.IND_R:
- bFmt.setRightMargin(self._mIndent)
- if tStyle & BlockFmt.IND_T:
- bFmt.setTextIndent(self._tIndent)
+ if tStyle & BlockFmt.IND_L:
+ bFmt.setLeftMargin(self._mIndent)
+ if tStyle & BlockFmt.IND_R:
+ bFmt.setRightMargin(self._mIndent)
+ if tStyle & BlockFmt.IND_T:
+ bFmt.setTextIndent(self._tIndent)
if tType in (BlockTyp.TEXT, BlockTyp.COMMENT, BlockTyp.KEYWORD):
newBlock(cursor, bFmt)
@@ -228,6 +232,9 @@ class ToQTextDocument(Tokenizer):
newBlock(cursor, bFmt)
cursor.insertText(nwUnicode.U_NBSP, self._charFmt)
+ if tStyle & BlockFmt.PBA:
+ self._insertNewPageMarker(cursor)
+
self._document.blockSignals(False)
return
@@ -386,6 +393,29 @@ class ToQTextDocument(Tokenizer):
return
+ def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
+ """Insert a new page marker."""
+ if self._newPage:
+ cursor.insertHtml("
")
+
+ hFmt = cursor.blockFormat()
+ hFmt.setBottomMargin(0.0)
+ hFmt.setLineHeight(75.0, QtPropLineHeight)
+ cursor.setBlockFormat(hFmt)
+
+ bFmt = QTextBlockFormat(self._blockFmt)
+ bFmt.setAlignment(QtAlignCenter)
+ bFmt.setTopMargin(0.0)
+ bFmt.setLineHeight(75.0, QtPropLineHeight)
+
+ cFmt = QTextCharFormat(self._charFmt)
+ cFmt.setFontPointSize(0.75*self._textFont.pointSizeF())
+ cFmt.setForeground(self._theme.comment)
+
+ newBlock(cursor, bFmt)
+ cursor.insertText(self._project.localLookup("New Page"), cFmt)
+ return
+
def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
"""Generate a heading style set."""
mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
diff --git a/novelwriter/types.py b/novelwriter/types.py
index e9db0876..e997f619 100644
--- a/novelwriter/types.py
+++ b/novelwriter/types.py
@@ -24,7 +24,10 @@ along with this program. If not, see .
from __future__ import annotations
from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextCursor, QTextFormat
+from PyQt5.QtGui import (
+ QColor, QFont, QPainter, QTextBlockFormat, QTextCharFormat, QTextCursor,
+ QTextFormat
+)
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QStyle
# Qt Alignment Flags
@@ -48,11 +51,13 @@ QtVAlignNormal = QTextCharFormat.VerticalAlignment.AlignNormal
QtVAlignSub = QTextCharFormat.VerticalAlignment.AlignSubScript
QtVAlignSuper = QTextCharFormat.VerticalAlignment.AlignSuperScript
-# Qt Page Break
+# Qt Text Formats
QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore
QtPageBreakAfter = QTextFormat.PageBreakFlag.PageBreak_AlwaysAfter
+QtPropLineHeight = QTextBlockFormat.LineHeightTypes.ProportionalHeight
+
# Qt Painter Types
QtTransparent = QColor(0, 0, 0, 0)
From fc6e0a577f59786de975dad224fede2014b70a1f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 7 Nov 2024 17:52:56 +0100
Subject: [PATCH 3/6] Add a switch to enable/disable showing page breaks in the
manuscript tool
---
novelwriter/assets/i18n/project_en_GB.json | 1 +
novelwriter/core/docbuild.py | 33 +++++++++-------------
novelwriter/core/options.py | 2 +-
novelwriter/tools/manuscript.py | 31 ++++++++++++++++----
tests/test_core/test_core_docbuild.py | 2 +-
tests/test_tools/test_tools_manuscript.py | 2 +-
6 files changed, 43 insertions(+), 28 deletions(-)
diff --git a/novelwriter/assets/i18n/project_en_GB.json b/novelwriter/assets/i18n/project_en_GB.json
index 39d52e6e..19cdda1e 100644
--- a/novelwriter/assets/i18n/project_en_GB.json
+++ b/novelwriter/assets/i18n/project_en_GB.json
@@ -14,6 +14,7 @@
"Objects": "Objects",
"Entities": "Entities",
"Custom": "Custom",
+ "New Page": "New Page",
"0": "Zero",
"1": "One",
"2": "Two",
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index 7161a794..96d88145 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -87,15 +87,6 @@ class NWBuildDocument:
"""
return self._cache
- ##
- # Setters
- ##
-
- def setBuildOutline(self, state: bool) -> None:
- """Turn on/off outline for builds."""
- self._outline = state
- return
-
##
# Special Methods
##
@@ -122,11 +113,12 @@ class NWBuildDocument:
self._queue.append(item.itemHandle)
return
- def iterBuildPreview(self) -> Iterable[tuple[int, bool]]:
+ def iterBuildPreview(self, newPage: bool) -> Iterable[tuple[int, bool]]:
"""Build a preview QTextDocument."""
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
+ makeObj.setShowNewPage(newPage)
self._outline = True
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
@@ -352,14 +344,17 @@ class NWBuildDocument:
tItem = self._project.tree[tHandle]
if isinstance(tItem, NWItem):
try:
- if tItem.isRootType() and not tItem.isNovelLike():
- bldObj.addRootHeading(tHandle)
- if convert:
- bldObj.doConvert()
- if self._count:
- bldObj.countStats()
- if self._outline:
- bldObj.buildOutline()
+ if tItem.isRootType():
+ if tItem.isNovelLike():
+ bldObj.setBreakNext()
+ else:
+ bldObj.addRootHeading(tHandle)
+ if convert:
+ bldObj.doConvert()
+ if self._count:
+ bldObj.countStats()
+ if self._outline:
+ bldObj.buildOutline()
elif tItem.isFileType():
bldObj.setText(tHandle)
bldObj.doPreProcessing()
@@ -370,8 +365,6 @@ class NWBuildDocument:
bldObj.buildOutline()
if convert:
bldObj.doConvert()
- else:
- logger.info(f"Build: Skipping '{tHandle}'")
except Exception:
self._error = f"Build: Failed to build '{tHandle}'"
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index 419e4ec2..35bd72d4 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -60,7 +60,7 @@ VALID_MAP: dict[str, set[str]] = {
},
"GuiManuscript": {
"winWidth", "winHeight", "optsWidth", "viewWidth", "listHeight",
- "detailsHeight", "detailsWidth", "detailsExpanded",
+ "detailsHeight", "detailsWidth", "detailsExpanded", "showNewPage",
},
"GuiManuscriptBuild": {
"winWidth", "winHeight", "fmtWidth", "sumWidth",
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index cad13292..25399053 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -48,6 +48,7 @@ from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
from novelwriter.extensions.progressbars import NProgressCircle
+from novelwriter.extensions.switch import NSwitch
from novelwriter.formats.tokenizer import HeadingFormatter
from novelwriter.formats.toqdoc import ToQTextDocument
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
@@ -87,6 +88,7 @@ class GuiManuscript(NToolDialog):
self.setMinimumWidth(CONFIG.pxInt(600))
self.setMinimumHeight(CONFIG.pxInt(500))
+ iPx = SHARED.theme.baseIconHeight
iSz = SHARED.theme.baseIconSize
wWin = CONFIG.pxInt(900)
hWin = CONFIG.pxInt(600)
@@ -191,15 +193,31 @@ class GuiManuscript(NToolDialog):
self.processBox.addWidget(self.btnBuild, 1, 0)
self.processBox.addWidget(self.btnClose, 1, 1)
+ # Preview Options
+ # ===============
+
+ self.swtNewPage = NSwitch(self, height=iPx)
+ self.swtNewPage.setChecked(pOptions.getBool("GuiManuscript", "showNewPage", True))
+ self.swtNewPage.clicked.connect(self._generatePreview)
+
+ self.lblNewPage = QLabel(self.tr("Show Page Breaks"), self)
+ self.lblNewPage.setBuddy(self.swtNewPage)
+
# Assemble GUI
# ============
self.docPreview = _PreviewWidget(self)
self.docStats = _StatsWidget(self)
+ self.docBar = QHBoxLayout()
+ self.docBar.addWidget(self.docStats, 1, QtAlignTop)
+ self.docBar.addWidget(self.lblNewPage, 0, QtAlignTop)
+ self.docBar.addWidget(self.swtNewPage, 0, QtAlignTop)
+ self.docBar.setContentsMargins(0, 0, 0, 0)
+
self.docBox = QVBoxLayout()
self.docBox.addWidget(self.docPreview, 1)
- self.docBox.addWidget(self.docStats, 0)
+ self.docBox.addLayout(self.docBar, 0)
self.docBox.setContentsMargins(0, 0, 0, 0)
self.docWdiget = QWidget(self)
@@ -344,6 +362,7 @@ class GuiManuscript(NToolDialog):
return
start = time()
+ showNewPage = self.swtNewPage.isChecked()
# Make sure editor content is saved before we start
SHARED.saveEditor()
@@ -352,7 +371,7 @@ class GuiManuscript(NToolDialog):
docBuild.queueAll()
self.docPreview.beginNewBuild(len(docBuild))
- for step, _ in docBuild.iterBuildPreview():
+ for step, _ in docBuild.iterBuildPreview(showNewPage):
self.docPreview.buildStep(step + 1)
QApplication.processEvents()
@@ -434,6 +453,7 @@ class GuiManuscript(NToolDialog):
detailsHeight = CONFIG.rpxInt(buildSplit[1])
detailsWidth = CONFIG.rpxInt(self.buildDetails.getColumnWidth())
detailsExpanded = self.buildDetails.getExpandedState()
+ showNewPage = self.swtNewPage.isChecked()
logger.debug("Saving State: GuiManuscript")
pOptions = SHARED.project.options
@@ -445,6 +465,7 @@ class GuiManuscript(NToolDialog):
pOptions.setValue("GuiManuscript", "detailsHeight", detailsHeight)
pOptions.setValue("GuiManuscript", "detailsWidth", detailsWidth)
pOptions.setValue("GuiManuscript", "detailsExpanded", detailsExpanded)
+ pOptions.setValue("GuiManuscript", "showNewPage", showNewPage)
pOptions.saveSettings()
return
@@ -945,7 +966,7 @@ class _StatsWidget(QWidget):
self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize, "unfold")
self.toggleButton.toggled.connect(self._toggleView)
- self._buildStatsPanel()
+ self._buildBottomPanel()
self.mainStack = QStackedWidget(self)
self.mainStack.addWidget(self.minWidget)
@@ -1010,8 +1031,8 @@ class _StatsWidget(QWidget):
# Internal Functions
##
- def _buildStatsPanel(self) -> None:
- """Build the minimal stats page."""
+ def _buildBottomPanel(self) -> None:
+ """Build the bottom page."""
mPx = CONFIG.pxInt(8)
hPx = CONFIG.pxInt(12)
vPx = CONFIG.pxInt(4)
diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py
index 04ec4e75..cbb6c52f 100644
--- a/tests/test_core/test_core_docbuild.py
+++ b/tests/test_core/test_core_docbuild.py
@@ -88,7 +88,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
build.unpack(BUILD_CONF)
docBuild = NWBuildDocument(project, build)
- docBuild.setBuildOutline(True)
+ docBuild._outline = True
docBuild.queueAll()
assert docBuild._outline is True
diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py
index df528751..6d94477a 100644
--- a/tests/test_tools/test_tools_manuscript.py
+++ b/tests/test_tools/test_tools_manuscript.py
@@ -48,7 +48,7 @@ def testToolManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
SHARED.project.storage.getDocument(C.hChapterDoc).writeDocument("## A Chapter\n\n\t\tHi")
- allText = "New Novel\nBy Jane Doe\nA Chapter\n\t\tHi"
+ allText = "New Novel\nBy Jane Doe\n\nNew Page\nA Chapter\n\t\tHi"
nwGUI.mainMenu.aBuildManuscript.activate(QAction.ActionEvent.Trigger)
qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(GuiManuscript) is not None, timeout=1000)
From d04a21b70f84c56af5c1de84f363bc051e046206 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 7 Nov 2024 17:53:19 +0100
Subject: [PATCH 4/6] Improve page break handling in the tokenizer
---
novelwriter/formats/tokenizer.py | 73 +++++++++++++++++---------------
1 file changed, 39 insertions(+), 34 deletions(-)
diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py
index 51bfaef1..c8598e37 100644
--- a/novelwriter/formats/tokenizer.py
+++ b/novelwriter/formats/tokenizer.py
@@ -166,6 +166,7 @@ class Tokenizer(ABC):
self._hFormatter = HeadingFormatter(self._project)
self._noSep = True # Flag to indicate that we don't want a scene separator
self._noIndent = False # Flag to disable text indent on next paragraph
+ self._forcePage = False # Force a page break on next token
# This File
self._isNovel = False # Document is a novel document
@@ -444,6 +445,11 @@ class Tokenizer(ABC):
self._classes["optional"] = self._theme.optional
return
+ def setBreakNext(self) -> None:
+ """Set a page break for next block."""
+ self._forcePage = True
+ return
+
def addRootHeading(self, tHandle: str) -> None:
"""Add a heading at the start of a new root folder."""
self._text = ""
@@ -531,7 +537,6 @@ class Tokenizer(ABC):
text = REGEX_PATTERNS.lineBreak.sub("\uffff", self._text)
nHead = 0
- breakNext = False
rawText = []
tHandle = self._handle or ""
tBlocks: list[T_Block] = [B_EMPTY]
@@ -546,11 +551,11 @@ class Tokenizer(ABC):
rawText.append("\n")
continue
- if breakNext:
- sAlign = BlockFmt.PBB
- breakNext = False
+ if self._forcePage:
+ tStyle = BlockFmt.PBB
+ self._forcePage = False
else:
- sAlign = BlockFmt.NONE
+ tStyle = BlockFmt.NONE
# Check Line Format
# =================
@@ -563,12 +568,12 @@ class Tokenizer(ABC):
# therefore proceed to check other formats.
if sLine in ("[newpage]", "[new page]"):
- breakNext = True
+ self._forcePage = True
continue
elif sLine == "[vspace]":
tBlocks.append(
- (BlockTyp.SKIP, "", "", [], sAlign)
+ (BlockTyp.SKIP, "", "", [], tStyle)
)
continue
@@ -576,7 +581,7 @@ class Tokenizer(ABC):
nSkip = checkInt(sLine[8:-1], 0)
if nSkip >= 1:
tBlocks.append(
- (BlockTyp.SKIP, "", "", [], sAlign)
+ (BlockTyp.SKIP, "", "", [], tStyle)
)
if nSkip > 1:
tBlocks += (nSkip - 1) * [
@@ -599,14 +604,14 @@ class Tokenizer(ABC):
if cStyle == nwComment.PLAIN and not self._doComments:
continue
- if doJustify and not sAlign & BlockFmt.ALIGNED:
- sAlign |= BlockFmt.JUSTIFY
+ if doJustify and not tStyle & BlockFmt.ALIGNED:
+ tStyle |= BlockFmt.JUSTIFY
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN):
bStyle = COMMENT_STYLE[cStyle]
tLine, tFmt = self._formatComment(bStyle, cKey, cText)
tBlocks.append((
- BlockTyp.COMMENT, "", tLine, tFmt, sAlign
+ BlockTyp.COMMENT, "", tLine, tFmt, tStyle
))
if keepRaw:
rawText.append(f"{aLine}\n")
@@ -627,7 +632,7 @@ class Tokenizer(ABC):
tTag, tLine, tFmt = self._formatMeta(aLine)
if tLine:
tBlocks.append((
- BlockTyp.KEYWORD, tTag[1:], tLine, tFmt, sAlign
+ BlockTyp.KEYWORD, tTag[1:], tLine, tFmt, tStyle
))
if keepRaw:
rawText.append(f"{aLine}\n")
@@ -646,16 +651,16 @@ class Tokenizer(ABC):
nHead += 1
tText = aLine[2:].strip()
tType = BlockTyp.HEAD1 if isPlain else BlockTyp.TITLE
- tStyle = BlockFmt.NONE if isPlain else self._titleStyle
sHide = self._hidePart if isPlain else False
+ if not (isPlain or isNovel and sHide):
+ tStyle |= self._titleStyle
if isNovel:
if sHide:
tText = ""
tType = BlockTyp.EMPTY
- tStyle = BlockFmt.NONE
elif isPlain:
tText = self._hFormatter.apply(self._fmtPart, tText, nHead)
- tStyle = self._partStyle
+ tStyle |= self._partStyle
if isPlain:
self._hFormatter.resetScene()
else:
@@ -682,7 +687,6 @@ class Tokenizer(ABC):
nHead += 1
tText = aLine[3:].strip()
tType = BlockTyp.HEAD2
- tStyle = BlockFmt.NONE
sHide = self._hideChapter if isPlain else self._hideUnNum
tFormat = self._fmtChapter if isPlain else self._fmtUnNum
if isNovel:
@@ -693,7 +697,7 @@ class Tokenizer(ABC):
tType = BlockTyp.EMPTY
else:
tText = self._hFormatter.apply(tFormat, tText, nHead)
- tStyle = self._chapterStyle
+ tStyle |= self._chapterStyle
self._hFormatter.resetScene()
self._noSep = True
@@ -719,7 +723,6 @@ class Tokenizer(ABC):
nHead += 1
tText = aLine[4:].strip()
tType = BlockTyp.HEAD3
- tStyle = BlockFmt.NONE
sHide = self._hideScene if isPlain else self._hideHScene
tFormat = self._fmtScene if isPlain else self._fmtHScene
if isNovel:
@@ -729,13 +732,13 @@ class Tokenizer(ABC):
tType = BlockTyp.EMPTY
else:
tText = self._hFormatter.apply(tFormat, tText, nHead)
- tStyle = self._sceneStyle
+ tStyle |= self._sceneStyle
if tText == "": # Empty Format
tType = BlockTyp.EMPTY if self._noSep else BlockTyp.SKIP
elif tText == tFormat: # Static Format
tText = "" if self._noSep else tText
tType = BlockTyp.EMPTY if self._noSep else BlockTyp.SEP
- tStyle = BlockFmt.NONE if self._noSep else BlockFmt.CENTRE
+ tStyle |= BlockFmt.NONE if self._noSep else BlockFmt.CENTRE
self._noSep = False
tBlocks.append((
@@ -755,7 +758,6 @@ class Tokenizer(ABC):
nHead += 1
tText = aLine[5:].strip()
tType = BlockTyp.HEAD4
- tStyle = BlockFmt.NONE
if isNovel:
if self._hideSection:
tText = ""
@@ -766,7 +768,7 @@ class Tokenizer(ABC):
tType = BlockTyp.SKIP
elif tText == self._fmtSection: # Static Format
tType = BlockTyp.SEP
- tStyle = BlockFmt.CENTRE
+ tStyle |= BlockFmt.CENTRE
tBlocks.append((
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
@@ -803,35 +805,38 @@ class Tokenizer(ABC):
bLine = bLine[:-1].rstrip(" ")
if alnLeft and alnRight:
- sAlign |= BlockFmt.CENTRE
+ tStyle |= BlockFmt.CENTRE
elif alnLeft:
- sAlign |= BlockFmt.LEFT
+ tStyle |= BlockFmt.LEFT
elif alnRight:
- sAlign |= BlockFmt.RIGHT
+ tStyle |= BlockFmt.RIGHT
if indLeft:
- sAlign |= BlockFmt.IND_L
+ tStyle |= BlockFmt.IND_L
if indRight:
- sAlign |= BlockFmt.IND_R
+ tStyle |= BlockFmt.IND_R
# Process formats
tLine, tFmt = self._extractFormats(bLine, hDialog=isNovel)
tBlocks.append((
- BlockTyp.TEXT, "", tLine, tFmt, sAlign
+ BlockTyp.TEXT, "", tLine, tFmt, tStyle
))
if keepRaw:
rawText.append(f"{aLine}\n")
# If we have content, turn off the first page flag
- if self._isFirst and len(tBlocks) > 1:
+ if self._isFirst and tBlocks:
self._isFirst = False # First document has been processed
# Make sure the blocks array doesn't start with a page break
- # on the very first page, adding a blank first page.
- if (cBlock := tBlocks[1])[4] & BlockFmt.PBB:
- tBlocks[1] = (
- cBlock[0], cBlock[1], cBlock[2], cBlock[3], cBlock[4] & ~BlockFmt.PBB
- )
+ # on the very first block, adding a blank first page.
+ for n, cBlock in enumerate(tBlocks):
+ if cBlock[0] != BlockTyp.EMPTY:
+ if cBlock[4] & BlockFmt.PBB:
+ tBlocks[n] = (
+ cBlock[0], cBlock[1], cBlock[2], cBlock[3], cBlock[4] & ~BlockFmt.PBB
+ )
+ break
# Always add an empty line at the end of the file
tBlocks.append(B_EMPTY)
From 9919e1a49e924006d97ed052791211ae006017dc Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 7 Nov 2024 18:31:08 +0100
Subject: [PATCH 5/6] Add new features to new project title page
---
novelwriter/core/coretools.py | 30 +++++++++---
.../coreTools_ProjectBuilderA_Project.md | 38 ++++++++++++++
.../coreTools_ProjectBuilderB_Project.md | 30 ++++++++++++
tests/test_core/test_core_coretools.py | 49 ++++++++++++++++---
4 files changed, 131 insertions(+), 16 deletions(-)
create mode 100644 tests/reference/coreTools_ProjectBuilderA_Project.md
create mode 100644 tests/reference/coreTools_ProjectBuilderB_Project.md
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index 131a0104..e2bdd342 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -39,7 +39,7 @@ from PyQt5.QtCore import QCoreApplication
from novelwriter import CONFIG, SHARED
from novelwriter.common import isHandle, minmax, simplified
-from novelwriter.constants import nwConst, nwFiles, nwItemClass
+from novelwriter.constants import nwConst, nwFiles, nwItemClass, nwStats
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
from novelwriter.core.storage import NWStorageCreate
@@ -428,7 +428,6 @@ class ProjectBuilder:
lblNewProject = self.tr("New Project")
lblTitlePage = self.tr("Title Page")
- lblByAuthors = self.tr("By")
# Settings
project.data.setUuid(None)
@@ -441,14 +440,29 @@ class ProjectBuilder:
# Add Root Folders
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
- novelTitle = project.data.name
-
- titlePage = f"#! {novelTitle}\n\n"
- if project.data.author:
- titlePage += f">> {lblByAuthors} {project.data.author} <<\n\n"
+ # Generate Title Page
aDoc = project.storage.getDocument(hTitlePage)
- aDoc.writeDocument(titlePage)
+ aDoc.writeDocument((
+ "{author}[br]\n"
+ "{address} 1[br]\n"
+ "{address} 2 <<\n"
+ "\n"
+ "[vspace:5]\n"
+ "\n"
+ "#! {title}\n"
+ "\n"
+ ">> **{by} {author}** <<\n"
+ "\n"
+ ">> {count}: [field:{field}] <<\n"
+ ).format(
+ author=project.data.author or "None",
+ address=self.tr("Address"),
+ title=project.data.name or "None",
+ by=self.tr("By"),
+ count=self.tr("Word Count"),
+ field=nwStats.WORDS_TEXT,
+ ))
# Create a project structure based on selected root folders
# and a number of chapters and scenes selected in the
diff --git a/tests/reference/coreTools_ProjectBuilderA_Project.md b/tests/reference/coreTools_ProjectBuilderA_Project.md
new file mode 100644
index 00000000..f1fe2be4
--- /dev/null
+++ b/tests/reference/coreTools_ProjectBuilderA_Project.md
@@ -0,0 +1,38 @@
+Jane Doe
+Address 1
+Address 2
+
+
+
+
+
+
+
+
+
+
+
+# Test Project A
+
+**By Jane Doe**
+
+Word Count: 11
+
+## Chapter 1
+
+* * *
+
+* * *
+
+## Chapter 2
+
+* * *
+
+* * *
+
+## Chapter 3
+
+* * *
+
+* * *
+
diff --git a/tests/reference/coreTools_ProjectBuilderB_Project.md b/tests/reference/coreTools_ProjectBuilderB_Project.md
new file mode 100644
index 00000000..386a773c
--- /dev/null
+++ b/tests/reference/coreTools_ProjectBuilderB_Project.md
@@ -0,0 +1,30 @@
+Jane Doe
+Address 1
+Address 2
+
+
+
+
+
+
+
+
+
+
+
+# Test Project B
+
+**By Jane Doe**
+
+Word Count: 11
+
+* * *
+
+* * *
+
+* * *
+
+* * *
+
+* * *
+
diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py
index 3827db7a..785b6b58 100644
--- a/tests/test_core/test_core_coretools.py
+++ b/tests/test_core/test_core_coretools.py
@@ -31,10 +31,13 @@ import pytest
from novelwriter import CONFIG
from novelwriter.constants import nwConst, nwFiles, nwItemClass
+from novelwriter.core.buildsettings import BuildSettings
from novelwriter.core.coretools import (
DocDuplicator, DocMerger, DocSearch, DocSplitter, ProjectBuilder
)
+from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.project import NWProject
+from novelwriter.enum import nwBuildFmt
from tests.mocked import causeOSError
from tests.tools import NWD_IGNORE, XML_IGNORE, C, buildTestProject, cmpFiles
@@ -507,10 +510,6 @@ def testCoreTools_ProjectBuilderA(monkeypatch, fncPath, tstPaths, mockGUI, mockR
"""Create a new project from a project dictionary, with chapters."""
monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
- projFile = fncPath / "nwProject.nwx"
- testFile = tstPaths.outDir / "coreTools_ProjectBuilderA_nwProject.nwx"
- compFile = tstPaths.refDir / "coreTools_ProjectBuilderA_nwProject.nwx"
-
data = {
"name": "Test Project A",
"author": "Jane Doe",
@@ -531,19 +530,34 @@ def testCoreTools_ProjectBuilderA(monkeypatch, fncPath, tstPaths, mockGUI, mockR
builder = ProjectBuilder()
assert builder.buildProject(data) is True
+ projFile = fncPath / "nwProject.nwx"
+ testFile = tstPaths.outDir / "coreTools_ProjectBuilderA_nwProject.nwx"
+ compFile = tstPaths.refDir / "coreTools_ProjectBuilderA_nwProject.nwx"
+
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
+ # Check Content
+ project = NWProject()
+ project.openProject(fncPath)
+
+ build = BuildSettings()
+
+ docBuild = NWBuildDocument(project, build)
+ docBuild.queueAll()
+
+ testFile = tstPaths.outDir / "coreTools_ProjectBuilderA_Project.md"
+ compFile = tstPaths.refDir / "coreTools_ProjectBuilderA_Project.md"
+
+ assert list(docBuild.iterBuildDocument(testFile, nwBuildFmt.EXT_MD))
+ assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
+
@pytest.mark.core
def testCoreTools_ProjectBuilderB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
"""Create a new project from a project dictionary, without chapters."""
monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
- projFile = fncPath / "nwProject.nwx"
- testFile = tstPaths.outDir / "coreTools_ProjectBuilderB_nwProject.nwx"
- compFile = tstPaths.refDir / "coreTools_ProjectBuilderB_nwProject.nwx"
-
data = {
"name": "Test Project B",
"author": "Jane Doe",
@@ -564,9 +578,28 @@ def testCoreTools_ProjectBuilderB(monkeypatch, fncPath, tstPaths, mockGUI, mockR
builder = ProjectBuilder()
assert builder.buildProject(data) is True
+ projFile = fncPath / "nwProject.nwx"
+ testFile = tstPaths.outDir / "coreTools_ProjectBuilderB_nwProject.nwx"
+ compFile = tstPaths.refDir / "coreTools_ProjectBuilderB_nwProject.nwx"
+
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
+ # Check Content
+ project = NWProject()
+ project.openProject(fncPath)
+
+ build = BuildSettings()
+
+ docBuild = NWBuildDocument(project, build)
+ docBuild.queueAll()
+
+ testFile = tstPaths.outDir / "coreTools_ProjectBuilderB_Project.md"
+ compFile = tstPaths.refDir / "coreTools_ProjectBuilderB_Project.md"
+
+ assert list(docBuild.iterBuildDocument(testFile, nwBuildFmt.EXT_MD))
+ assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
+
@pytest.mark.core
def testCoreTools_ProjectBuilderCopyPlain(monkeypatch, caplog, mockGUI, prjLipsum, fncPath):
From 87512eb13bc639f4be5a1d7f75c9729e0aa1d978 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 7 Nov 2024 18:33:53 +0100
Subject: [PATCH 6/6] Rename variable
---
novelwriter/formats/tokenizer.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py
index c8598e37..b7eba033 100644
--- a/novelwriter/formats/tokenizer.py
+++ b/novelwriter/formats/tokenizer.py
@@ -166,7 +166,7 @@ class Tokenizer(ABC):
self._hFormatter = HeadingFormatter(self._project)
self._noSep = True # Flag to indicate that we don't want a scene separator
self._noIndent = False # Flag to disable text indent on next paragraph
- self._forcePage = False # Force a page break on next token
+ self._breakNext = False # Add a page break on next token
# This File
self._isNovel = False # Document is a novel document
@@ -447,7 +447,7 @@ class Tokenizer(ABC):
def setBreakNext(self) -> None:
"""Set a page break for next block."""
- self._forcePage = True
+ self._breakNext = True
return
def addRootHeading(self, tHandle: str) -> None:
@@ -551,9 +551,9 @@ class Tokenizer(ABC):
rawText.append("\n")
continue
- if self._forcePage:
+ if self._breakNext:
tStyle = BlockFmt.PBB
- self._forcePage = False
+ self._breakNext = False
else:
tStyle = BlockFmt.NONE
@@ -568,7 +568,7 @@ class Tokenizer(ABC):
# therefore proceed to check other formats.
if sLine in ("[newpage]", "[new page]"):
- self._forcePage = True
+ self._breakNext = True
continue
elif sLine == "[vspace]":