From f2d2b4c4f2dbc6fcc71625ea17411980d508ae43 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:17:31 +0100 Subject: [PATCH 01/11] Generalise the manuscript stats count values --- novelwriter/constants.py | 63 ++++++++++++++++-- novelwriter/formats/tokenizer.py | 47 ++++++------- novelwriter/gui/dochighlight.py | 1 + novelwriter/tools/manuscript.py | 111 ++++++++++++++++--------------- 4 files changed, 140 insertions(+), 82 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index c50c804c..83c22ac6 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -67,7 +67,7 @@ class nwRegEx: FMT_EB = r"(? None: """Count stats on the tokenized text.""" - titleCount = self._counts.get("titleCount", 0) - paragraphCount = self._counts.get("paragraphCount", 0) + titleCount = self._counts.get(nwStats.TITLES, 0) + paragraphCount = self._counts.get(nwStats.PARAGRAPHS, 0) - allWords = self._counts.get("allWords", 0) - textWords = self._counts.get("textWords", 0) - titleWords = self._counts.get("titleWords", 0) + allWords = self._counts.get(nwStats.WORDS_ALL, 0) + textWords = self._counts.get(nwStats.WORDS_TEXT, 0) + titleWords = self._counts.get(nwStats.WORDS_TITLE, 0) - allChars = self._counts.get("allChars", 0) - textChars = self._counts.get("textChars", 0) - titleChars = self._counts.get("titleChars", 0) + allChars = self._counts.get(nwStats.CHARS_ALL, 0) + textChars = self._counts.get(nwStats.CHARS_TEXT, 0) + titleChars = self._counts.get(nwStats.CHARS_TITLE, 0) - allWordChars = self._counts.get("allWordChars", 0) - textWordChars = self._counts.get("textWordChars", 0) - titleWordChars = self._counts.get("titleWordChars", 0) + allWordChars = self._counts.get(nwStats.WCHARS_ALL, 0) + textWordChars = self._counts.get(nwStats.WCHARS_TEXT, 0) + titleWordChars = self._counts.get(nwStats.WCHARS_TITLE, 0) for tType, _, tText, _, _ in self._blocks: tText = tText.replace(nwUnicode.U_ENDASH, " ") @@ -1006,20 +1007,20 @@ class Tokenizer(ABC): allChars += len(tText) allWordChars += len("".join(words)) - self._counts["titleCount"] = titleCount - self._counts["paragraphCount"] = paragraphCount + self._counts[nwStats.TITLES] = titleCount + self._counts[nwStats.PARAGRAPHS] = paragraphCount - self._counts["allWords"] = allWords - self._counts["textWords"] = textWords - self._counts["titleWords"] = titleWords + self._counts[nwStats.WORDS_ALL] = allWords + self._counts[nwStats.WORDS_TEXT] = textWords + self._counts[nwStats.WORDS_TITLE] = titleWords - self._counts["allChars"] = allChars - self._counts["textChars"] = textChars - self._counts["titleChars"] = titleChars + self._counts[nwStats.CHARS_ALL] = allChars + self._counts[nwStats.CHARS_TEXT] = textChars + self._counts[nwStats.CHARS_TITLE] = titleChars - self._counts["allWordChars"] = allWordChars - self._counts["textWordChars"] = textWordChars - self._counts["titleWordChars"] = titleWordChars + self._counts[nwStats.WCHARS_ALL] = allWordChars + self._counts[nwStats.WCHARS_TEXT] = textWordChars + self._counts[nwStats.WCHARS_TITLE] = titleWordChars return diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 24c4d373..89436579 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -523,6 +523,7 @@ class TextBlockData(QTextBlockUserData): self._metaData.append((s, e, res.group(0), "url")) self._text = text + self._offset = offset return diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 698ad8bc..af8f5a3b 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -43,6 +43,7 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import fuzzyTime +from novelwriter.constants import nwLabels, nwStats, trConst from novelwriter.core.buildsettings import BuildCollection, BuildSettings from novelwriter.core.docbuild import NWBuildDocument from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog @@ -928,8 +929,7 @@ class _StatsWidget(QWidget): self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize, "unfold") self.toggleButton.toggled.connect(self._toggleView) - self._buildMinimal() - self._buildMaximal() + self._buildStatsPanel() self.mainStack = QStackedWidget(self) self.mainStack.addWidget(self.minWidget) @@ -949,23 +949,23 @@ class _StatsWidget(QWidget): def updateStats(self, data: dict[str, int]) -> None: """Update the stats values from a Tokenizer stats dict.""" # Minimal - self.minWordCount.setText("{0:n}".format(data.get("allWords", 0))) - self.minCharCount.setText("{0:n}".format(data.get("allChars", 0))) + self.minWordCount.setText("{0:n}".format(data.get(nwStats.WORDS_ALL, 0))) + self.minCharCount.setText("{0:n}".format(data.get(nwStats.CHARS_ALL, 0))) # Maximal - self.maxTotalWords.setText("{0:n}".format(data.get("allWords", 0))) - self.maxHeadWords.setText("{0:n}".format(data.get("titleWords", 0))) - self.maxTextWords.setText("{0:n}".format(data.get("textWords", 0))) - self.maxTitleCount.setText("{0:n}".format(data.get("titleCount", 0))) - self.maxParCount.setText("{0:n}".format(data.get("paragraphCount", 0))) + self.maxTotalWords.setText("{0:n}".format(data.get(nwStats.WORDS_ALL, 0))) + self.maxHeadWords.setText("{0:n}".format(data.get(nwStats.WORDS_TITLE, 0))) + self.maxTextWords.setText("{0:n}".format(data.get(nwStats.WORDS_TITLE, 0))) + self.maxTitleCount.setText("{0:n}".format(data.get(nwStats.TITLES, 0))) + self.maxParCount.setText("{0:n}".format(data.get(nwStats.PARAGRAPHS, 0))) - self.maxTotalChars.setText("{0:n}".format(data.get("allChars", 0))) - self.maxHeaderChars.setText("{0:n}".format(data.get("titleChars", 0))) - self.maxTextChars.setText("{0:n}".format(data.get("textChars", 0))) + self.maxTotalChars.setText("{0:n}".format(data.get(nwStats.CHARS_ALL, 0))) + self.maxHeaderChars.setText("{0:n}".format(data.get(nwStats.CHARS_TITLE, 0))) + self.maxTextChars.setText("{0:n}".format(data.get(nwStats.CHARS_TEXT, 0))) - self.maxTotalWordChars.setText("{0:n}".format(data.get("allWordChars", 0))) - self.maxHeadWordChars.setText("{0:n}".format(data.get("titleWordChars", 0))) - self.maxTextWordChars.setText("{0:n}".format(data.get("textWordChars", 0))) + self.maxTotalWordChars.setText("{0:n}".format(data.get(nwStats.WCHARS_ALL, 0))) + self.maxHeadWordChars.setText("{0:n}".format(data.get(nwStats.WCHARS_TITLE, 0))) + self.maxTextWordChars.setText("{0:n}".format(data.get(nwStats.WCHARS_TEXT, 0))) return @@ -994,37 +994,29 @@ class _StatsWidget(QWidget): # Internal Functions ## - def _buildMinimal(self) -> None: + def _buildStatsPanel(self) -> None: """Build the minimal stats page.""" mPx = CONFIG.pxInt(8) - - self.lblWordCount = QLabel(self.tr("Words"), self) - self.minWordCount = QLabel(self) - - self.lblCharCount = QLabel(self.tr("Characters"), self) - self.minCharCount = QLabel(self) - - # Assemble - self.minLayout = QHBoxLayout() - self.minLayout.addWidget(self.lblWordCount) - self.minLayout.addWidget(self.minWordCount) - self.minLayout.addSpacing(mPx) - self.minLayout.addWidget(self.lblCharCount) - self.minLayout.addWidget(self.minCharCount) - self.minLayout.addStretch(1) - self.minLayout.setSpacing(mPx) - self.minLayout.setContentsMargins(0, 0, 0, 0) - - self.minWidget.setLayout(self.minLayout) - - return - - def _buildMaximal(self) -> None: - """Build the maximal stats page.""" hPx = CONFIG.pxInt(12) vPx = CONFIG.pxInt(4) - # Left Column + trAllChars = trConst(nwLabels.STATS_NAME[nwStats.CHARS_ALL]) + trTextChars = trConst(nwLabels.STATS_NAME[nwStats.CHARS_TEXT]) + trTitleChars = trConst(nwLabels.STATS_NAME[nwStats.CHARS_TITLE]) + trParagraphCount = trConst(nwLabels.STATS_NAME[nwStats.PARAGRAPHS]) + trTitleCount = trConst(nwLabels.STATS_NAME[nwStats.TITLES]) + trAllWordChars = trConst(nwLabels.STATS_NAME[nwStats.WCHARS_ALL]) + trTextWordChars = trConst(nwLabels.STATS_NAME[nwStats.WCHARS_TEXT]) + trTitleWordChars = trConst(nwLabels.STATS_NAME[nwStats.WCHARS_TITLE]) + trAllWords = trConst(nwLabels.STATS_NAME[nwStats.WORDS_ALL]) + trTextWords = trConst(nwLabels.STATS_NAME[nwStats.WORDS_TEXT]) + trTitleWords = trConst(nwLabels.STATS_NAME[nwStats.WORDS_TITLE]) + + # Minimal Form + self.minWordCount = QLabel(self) + self.minCharCount = QLabel(self) + + # Maximal Form, Left Column self.maxTotalWords = QLabel(self) self.maxHeadWords = QLabel(self) self.maxTextWords = QLabel(self) @@ -1038,20 +1030,19 @@ class _StatsWidget(QWidget): self.maxParCount.setAlignment(QtAlignRight) self.leftForm = QFormLayout() - self.leftForm.addRow(self.tr("Words"), self.maxTotalWords) - self.leftForm.addRow(self.tr("Words in Headings"), self.maxHeadWords) - self.leftForm.addRow(self.tr("Words in Text"), self.maxTextWords) + self.leftForm.addRow(trAllWords, self.maxTotalWords) + self.leftForm.addRow(trTitleWords, self.maxHeadWords) + self.leftForm.addRow(trTextWords, self.maxTextWords) self.leftForm.addRow("", QLabel(self)) - self.leftForm.addRow(self.tr("Headings"), self.maxTitleCount) - self.leftForm.addRow(self.tr("Paragraphs"), self.maxParCount) + self.leftForm.addRow(trTitleCount, self.maxTitleCount) + self.leftForm.addRow(trParagraphCount, self.maxParCount) self.leftForm.setHorizontalSpacing(hPx) self.leftForm.setVerticalSpacing(vPx) - # Right Column + # Maximal Form, Right Column self.maxTotalChars = QLabel(self) self.maxHeaderChars = QLabel(self) self.maxTextChars = QLabel(self) - self.maxTotalWordChars = QLabel(self) self.maxHeadWordChars = QLabel(self) self.maxTextWordChars = QLabel(self) @@ -1059,22 +1050,31 @@ class _StatsWidget(QWidget): self.maxTotalChars.setAlignment(QtAlignRight) self.maxHeaderChars.setAlignment(QtAlignRight) self.maxTextChars.setAlignment(QtAlignRight) - self.maxTotalWordChars.setAlignment(QtAlignRight) self.maxHeadWordChars.setAlignment(QtAlignRight) self.maxTextWordChars.setAlignment(QtAlignRight) self.rightForm = QFormLayout() - self.rightForm.addRow(self.tr("Characters"), self.maxTotalChars) - self.rightForm.addRow(self.tr("Characters in Headings"), self.maxHeaderChars) - self.rightForm.addRow(self.tr("Characters in Text"), self.maxTextChars) - self.rightForm.addRow(self.tr("Characters, No Spaces"), self.maxTotalWordChars) - self.rightForm.addRow(self.tr("Characters in Headings, No Spaces"), self.maxHeadWordChars) - self.rightForm.addRow(self.tr("Characters in Text, No Spaces"), self.maxTextWordChars) + self.rightForm.addRow(trAllChars, self.maxTotalChars) + self.rightForm.addRow(trTitleChars, self.maxHeaderChars) + self.rightForm.addRow(trTextChars, self.maxTextChars) + self.rightForm.addRow(trAllWordChars, self.maxTotalWordChars) + self.rightForm.addRow(trTitleWordChars, self.maxHeadWordChars) + self.rightForm.addRow(trTextWordChars, self.maxTextWordChars) self.rightForm.setHorizontalSpacing(hPx) self.rightForm.setVerticalSpacing(vPx) # Assemble + self.minLayout = QHBoxLayout() + self.minLayout.addWidget(QLabel(trAllWords, self)) + self.minLayout.addWidget(self.minWordCount) + self.minLayout.addSpacing(mPx) + self.minLayout.addWidget(QLabel(trAllChars, self)) + self.minLayout.addWidget(self.minCharCount) + self.minLayout.addStretch(1) + self.minLayout.setSpacing(mPx) + self.minLayout.setContentsMargins(0, 0, 0, 0) + self.maxLayout = QHBoxLayout() self.maxLayout.addLayout(self.leftForm) self.maxLayout.addLayout(self.rightForm) @@ -1082,6 +1082,7 @@ class _StatsWidget(QWidget): self.maxLayout.setSpacing(CONFIG.pxInt(32)) self.maxLayout.setContentsMargins(0, 0, 0, 0) + self.minWidget.setLayout(self.minLayout) self.maxWidget.setLayout(self.maxLayout) return From 857b6ddc55c91d0e5a4816bc943b7a886e6daa6a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:24:24 +0100 Subject: [PATCH 02/11] Update main menu --- novelwriter/gui/mainmenu.py | 34 +- tests/test_gui/test_gui_mainmenu.py | 505 +++++++++++++--------------- 2 files changed, 257 insertions(+), 282 deletions(-) diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 449b9b70..4714547d 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -34,7 +34,10 @@ from PyQt5.QtWidgets import QAction, QMenuBar from novelwriter import CONFIG, SHARED from novelwriter.common import openExternalPath, qtLambda -from novelwriter.constants import nwConst, nwKeyWords, nwLabels, nwStyles, nwUnicode, trConst +from novelwriter.constants import ( + nwConst, nwKeyWords, nwLabels, nwShortcode, nwStats, nwStyles, nwUnicode, + trConst +) from novelwriter.enum import nwDocAction, nwDocInsert, nwFocus, nwView from novelwriter.extensions.eventfilters import StatusTipFilter @@ -571,25 +574,11 @@ class GuiMainMenu(QMenuBar): # Insert > Tags and References self.mInsKeywords = self.insMenu.addMenu(self.tr("Tags and References")) - self.mInsKWItems = {} - self.mInsKWItems[nwKeyWords.TAG_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, G") - self.mInsKWItems[nwKeyWords.POV_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, V") - self.mInsKWItems[nwKeyWords.FOCUS_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, F") - self.mInsKWItems[nwKeyWords.CHAR_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, C") - self.mInsKWItems[nwKeyWords.PLOT_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, P") - self.mInsKWItems[nwKeyWords.TIME_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, T") - self.mInsKWItems[nwKeyWords.WORLD_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, L") - self.mInsKWItems[nwKeyWords.OBJECT_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, O") - self.mInsKWItems[nwKeyWords.ENTITY_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, E") - self.mInsKWItems[nwKeyWords.CUSTOM_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, X") - self.mInsKWItems[nwKeyWords.MENTION_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, M") - for key in self.mInsKWItems: - action = self.mInsKWItems[key][0] - action.setText(trConst(nwLabels.KEY_NAME[key])) - action.setShortcut(self.mInsKWItems[key][1]) + for key in nwKeyWords.ALL_KEYS: + action = self.mInsKeywords.addAction(trConst(nwLabels.KEY_NAME[key])) + action.setShortcut(nwLabels.KEY_SHORTCUT[key]) action.triggered.connect(qtLambda(self.requestDocKeyWordInsert.emit, key)) - self.mInsKeywords.addAction(action) - self.mainGui.addAction(self.mInsKWItems[key][0]) + self.mainGui.addAction(action) # Insert > Special Comments self.mInsComments = self.insMenu.addMenu(self.tr("Special Comments")) @@ -610,6 +599,13 @@ class GuiMainMenu(QMenuBar): ) self.mainGui.addAction(self.aInsShort) + # Insert > Word/Character Count + self.mInsField = self.insMenu.addMenu(self.tr("Word/Character Count")) + for field in nwStats.ALL_FIELDS: + value = nwShortcode.FIELD_VALUE.format(field) + action = self.mInsField.addAction(trConst(nwLabels.STATS_NAME[field])) + action.triggered.connect(qtLambda(self.requestDocInsertText.emit, value)) + # Insert > Breaks and Vertical Space self.mInsBreaks = self.insMenu.addMenu(self.tr("Breaks and Vertical Space")) diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 32a13d55..34d6901b 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -29,7 +29,7 @@ from PyQt5.QtGui import QDesktopServices, QTextBlock, QTextCursor from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox from novelwriter import CONFIG, SHARED -from novelwriter.constants import nwKeyWords, nwUnicode +from novelwriter.constants import nwKeyWords, nwShortcode, nwStats, nwUnicode from novelwriter.enum import nwDocAction, nwDocInsert from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.types import QtKeepAnchor, QtMoveRight @@ -70,161 +70,163 @@ def testGuiMainMenu_Slots(qtbot, monkeypatch, nwGUI, projPath): def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): """Test the main menu Edit and Format entries.""" monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) + mainMenu = nwGUI.mainMenu + docEditor = nwGUI.docEditor # Test Document Action with No Project - assert nwGUI.docEditor.docAction(nwDocAction.COPY) is False + assert docEditor.docAction(nwDocAction.COPY) is False assert nwGUI.openProject(prjLipsum) is True # Split By Chapter assert nwGUI.openDocument("4c4f28287af27") is True - nwGUI.docEditor.setCursorPosition(57) - cleanText = nwGUI.docEditor.getText()[54:101] + docEditor.setCursorPosition(57) + cleanText = docEditor.getText()[54:101] # Bold - nwGUI.mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) fmtStr = "**Pellentesque** nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:105] == fmtStr - nwGUI.mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[54:105] == fmtStr + mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Italic - nwGUI.mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) fmtStr = "_Pellentesque_ nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:103] == fmtStr - nwGUI.mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[54:103] == fmtStr + mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Strikethrough - nwGUI.mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger) fmtStr = "~~Pellentesque~~ nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:105] == fmtStr - nwGUI.mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[54:105] == fmtStr + mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Should get us back to plain - nwGUI.mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) - nwGUI.mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) - nwGUI.mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) - nwGUI.mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Double Quotes - nwGUI.mainMenu.aFmtDQuote.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtDQuote.activate(QAction.ActionEvent.Trigger) fmtStr = "“Pellentesque” nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:103] == fmtStr - nwGUI.mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[54:103] == fmtStr + mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Single Quotes - nwGUI.mainMenu.aFmtSQuote.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtSQuote.activate(QAction.ActionEvent.Trigger) fmtStr = "‘Pellentesque’ nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:103] == fmtStr - nwGUI.mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[54:103] == fmtStr + mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Block Formats # ============= # cSpell:ignore Pellentesque erat nulla posuere commodo - nwGUI.docEditor.setCursorPosition(57) + docEditor.setCursorPosition(57) # Header 1 - nwGUI.mainMenu.aFmtHead1.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtHead1.activate(QAction.ActionEvent.Trigger) fmtStr = "# Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:103] == fmtStr + assert docEditor.getText()[54:103] == fmtStr # Header 2 - nwGUI.mainMenu.aFmtHead2.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtHead2.activate(QAction.ActionEvent.Trigger) fmtStr = "## Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:104] == fmtStr + assert docEditor.getText()[54:104] == fmtStr # Header 3 - nwGUI.mainMenu.aFmtHead3.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtHead3.activate(QAction.ActionEvent.Trigger) fmtStr = "### Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:105] == fmtStr + assert docEditor.getText()[54:105] == fmtStr # Header 4 - nwGUI.mainMenu.aFmtHead4.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtHead4.activate(QAction.ActionEvent.Trigger) fmtStr = "#### Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:106] == fmtStr + assert docEditor.getText()[54:106] == fmtStr # Title Format - nwGUI.mainMenu.aFmtTitle.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtTitle.activate(QAction.ActionEvent.Trigger) fmtStr = "#! Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:104] == fmtStr + assert docEditor.getText()[54:104] == fmtStr # Unnumbered Chapter - nwGUI.mainMenu.aFmtUnNum.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtUnNum.activate(QAction.ActionEvent.Trigger) fmtStr = "##! Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:105] == fmtStr + assert docEditor.getText()[54:105] == fmtStr # Hard Scene - nwGUI.mainMenu.aFmtHardSc.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtHardSc.activate(QAction.ActionEvent.Trigger) fmtStr = "###! Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:106] == fmtStr + assert docEditor.getText()[54:106] == fmtStr # Clear Format - nwGUI.mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Comment On - nwGUI.mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger) fmtStr = "% Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:103] == fmtStr + assert docEditor.getText()[54:103] == fmtStr # Comment Off - nwGUI.mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Check comment with no space before text - nwGUI.docEditor.setCursorPosition(54) - nwGUI.docEditor.insertText("%") + docEditor.setCursorPosition(54) + docEditor.insertText("%") fmtStr = "%Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:102] == fmtStr + assert docEditor.getText()[54:102] == fmtStr - nwGUI.mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Undo/Redo - nwGUI.mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) + mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) fmtStr = "%Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[54:102] == fmtStr - nwGUI.mainMenu.aEditRedo.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[54:102] == fmtStr + mainMenu.aEditRedo.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:101] == cleanText # Cut, Copy and Paste - nwGUI.docEditor.setCursorPosition(54) - nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor) + docEditor.setCursorPosition(54) + docEditor._makeSelection(QTextCursor.WordUnderCursor) - nwGUI.mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:104] == ( + mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:104] == ( " nec erat ut nulla posuere commodo. Curabitur nisi" ) - nwGUI.mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:104] == ( + mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:104] == ( "Pellentesque nec erat ut nulla posuere commodo. Cu" ) - nwGUI.docEditor.setCursorPosition(54) - nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor) + docEditor.setCursorPosition(54) + docEditor._makeSelection(QTextCursor.WordUnderCursor) - nwGUI.mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:104] == ( + mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:104] == ( "Pellentesque nec erat ut nulla posuere commodo. Cu" ) - nwGUI.docEditor.setCursorPosition(54) - nwGUI.mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[54:104] == ( + docEditor.setCursorPosition(54) + mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[54:104] == ( "PellentesquePellentesque nec erat ut nulla posuere" ) - nwGUI.mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) + mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) # Select Paragraph/All - nwGUI.docEditor.setCursorPosition(57) - nwGUI.mainMenu.aSelectPar.activate(QAction.ActionEvent.Trigger) - cursor = nwGUI.docEditor.textCursor() + docEditor.setCursorPosition(57) + mainMenu.aSelectPar.activate(QAction.ActionEvent.Trigger) + cursor = docEditor.textCursor() assert cursor.selectedText() == ( "Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta " "imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit " @@ -235,78 +237,78 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): "nunc lacus, imperdiet nec posuere ac, interdum non lectus." ) - nwGUI.docEditor.setCursorPosition(57) - nwGUI.mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger) - cursor = nwGUI.docEditor.textCursor() + docEditor.setCursorPosition(57) + mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger) + cursor = docEditor.textCursor() assert len(cursor.selectedText()) == 1910 # Clear the Text - nwGUI.docEditor.clear() - assert nwGUI.docEditor.isEmpty + docEditor.clear() + assert docEditor.isEmpty # Alignment & Indent # ================== cleanText = "A single, short paragraph.\n\n" - nwGUI.docEditor.setPlainText(cleanText) - nwGUI.docEditor.setCursorPosition(0) + docEditor.setPlainText(cleanText) + docEditor.setCursorPosition(0) # Left Align - nwGUI.mainMenu.aFmtAlignLeft.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtAlignLeft.activate(QAction.ActionEvent.Trigger) fmtStr = "A single, short paragraph. <<" - assert nwGUI.docEditor.getText()[:29] == fmtStr + assert docEditor.getText()[:29] == fmtStr # Right Align - nwGUI.mainMenu.aFmtAlignRight.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtAlignRight.activate(QAction.ActionEvent.Trigger) fmtStr = ">> A single, short paragraph." - assert nwGUI.docEditor.getText()[:29] == fmtStr + assert docEditor.getText()[:29] == fmtStr # Centre Align - nwGUI.mainMenu.aFmtAlignCentre.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtAlignCentre.activate(QAction.ActionEvent.Trigger) fmtStr = ">> A single, short paragraph. <<" - assert nwGUI.docEditor.getText()[:32] == fmtStr + assert docEditor.getText()[:32] == fmtStr # Left Indent - nwGUI.mainMenu.aFmtIndentLeft.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtIndentLeft.activate(QAction.ActionEvent.Trigger) fmtStr = "> A single, short paragraph." - assert nwGUI.docEditor.getText()[:28] == fmtStr + assert docEditor.getText()[:28] == fmtStr # Right Indent - nwGUI.mainMenu.aFmtIndentRight.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtIndentRight.activate(QAction.ActionEvent.Trigger) fmtStr = "> A single, short paragraph. <" - assert nwGUI.docEditor.getText()[:30] == fmtStr + assert docEditor.getText()[:30] == fmtStr # No Format - nwGUI.mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText()[:30] == cleanText + mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText()[:30] == cleanText # Other Checks # Replace Quotes - nwGUI.docEditor.setPlainText(( + docEditor.setPlainText(( "### New Text\n\n" "Text with 'single' quotes and 'tricky stuff's'.\n\n" "Also text with \"double\" quotes which are \"less tricky\".\n\n" )) - nwGUI.mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger) - nwGUI.mainMenu.aFmtReplSng.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == ( + mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtReplSng.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == ( "### New Text\n\n" "Text with ‘single’ quotes and ‘tricky stuff’s’.\n\n" "Also text with \"double\" quotes which are \"less tricky\".\n\n" ) - nwGUI.mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger) - nwGUI.mainMenu.aFmtReplDbl.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == ( + mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger) + mainMenu.aFmtReplDbl.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == ( "### New Text\n\n" "Text with ‘single’ quotes and ‘tricky stuff’s’.\n\n" "Also text with “double” quotes which are “less tricky”.\n\n" ) # Remove in-paragraph line breaks - nwGUI.docEditor.setPlainText(( + docEditor.setPlainText(( "### New Text\n\n" "@char: Someone\n" "@location: Somewhere\n\n" @@ -314,8 +316,8 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): "Here is some text\non multiple\nlines.\n\n" "With another paragraph\nhere." )) - nwGUI.mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == ( + mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == ( "### New Text\n\n" "@char: Someone\n" "@location: Somewhere\n\n" @@ -324,7 +326,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): "With another paragraph here.\n" ) - nwGUI.docEditor.setPlainText(( + docEditor.setPlainText(( "### New Text\n\n" "@char: Someone\n" "@location: Somewhere\n\n" @@ -332,12 +334,12 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): "Here is some text\non multiple\nlines.\n\n" "With another paragraph\nhere." )) - cursor = nwGUI.docEditor.textCursor() + cursor = docEditor.textCursor() cursor.setPosition(74) cursor.movePosition(QtMoveRight, QtKeepAnchor, 29) - nwGUI.docEditor.setTextCursor(cursor) - nwGUI.mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == ( + docEditor.setTextCursor(cursor) + mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == ( "### New Text\n\n" "@char: Someone\n" "@location: Somewhere\n\n" @@ -347,10 +349,10 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): ) # Test Invalid Document Action - assert not nwGUI.docEditor.docAction(nwDocAction.NO_ACTION) + assert not docEditor.docAction(nwDocAction.NO_ACTION) # Test Invalid Formats - nwGUI.docEditor.setPlainText(( + docEditor.setPlainText(( "### New Text\n\n" "@tag: Bod\n\n" "Text with 'single' quotes and 'tricky stuff's'.\n\n" @@ -358,15 +360,15 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): )) # Cannot Format Tag - nwGUI.docEditor.setCursorPosition(17) - assert not nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_TXT) + docEditor.setCursorPosition(17) + assert not docEditor._formatBlock(nwDocAction.BLOCK_TXT) # Invalid Action - nwGUI.docEditor.setCursorPosition(30) - assert not nwGUI.docEditor._formatBlock(nwDocAction.NO_ACTION) + docEditor.setCursorPosition(30) + assert not docEditor._formatBlock(nwDocAction.NO_ACTION) # Ensure No Changes - assert nwGUI.docEditor.getText() == ( + assert docEditor.getText() == ( "### New Text\n\n" "@tag: Bod\n\n" "Text with 'single' quotes and 'tricky stuff's'.\n\n" @@ -383,198 +385,175 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None assert nwGUI.openDocument(C.hSceneDoc) is True - nwGUI.docEditor.clear() + mainMenu = nwGUI.mainMenu + docEditor = nwGUI.docEditor + docEditor.clear() # Test Faulty Inserts - nwGUI.docEditor.insertText("hello world") - assert nwGUI.docEditor.getText() == "hello world" - nwGUI.docEditor.clear() + docEditor.insertText("hello world") + assert docEditor.getText() == "hello world" + docEditor.clear() - nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) - assert nwGUI.docEditor.isEmpty + docEditor.insertText(nwDocInsert.NO_INSERT) + assert docEditor.isEmpty - nwGUI.docEditor.insertText(None) - assert nwGUI.docEditor.isEmpty + docEditor.insertText(None) + assert docEditor.isEmpty # qtbot.stop() - nwGUI.docEditor.clear() + docEditor.clear() # Check Menu Entries - nwGUI.mainMenu.aInsENDash.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_ENDASH - nwGUI.docEditor.clear() + mainMenu.aInsENDash.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_ENDASH + docEditor.clear() - nwGUI.mainMenu.aInsEMDash.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_EMDASH - nwGUI.docEditor.clear() + mainMenu.aInsEMDash.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_EMDASH + docEditor.clear() - nwGUI.mainMenu.aInsHorBar.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_HBAR - nwGUI.docEditor.clear() + mainMenu.aInsHorBar.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_HBAR + docEditor.clear() - nwGUI.mainMenu.aInsFigDash.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_FGDASH - nwGUI.docEditor.clear() + mainMenu.aInsFigDash.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_FGDASH + docEditor.clear() - nwGUI.mainMenu.aInsQuoteLS.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == CONFIG.fmtSQuoteOpen - nwGUI.docEditor.clear() + mainMenu.aInsQuoteLS.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == CONFIG.fmtSQuoteOpen + docEditor.clear() - nwGUI.mainMenu.aInsQuoteRS.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == CONFIG.fmtSQuoteClose - nwGUI.docEditor.clear() + mainMenu.aInsQuoteRS.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == CONFIG.fmtSQuoteClose + docEditor.clear() - nwGUI.mainMenu.aInsQuoteLD.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == CONFIG.fmtDQuoteOpen - nwGUI.docEditor.clear() + mainMenu.aInsQuoteLD.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == CONFIG.fmtDQuoteOpen + docEditor.clear() - nwGUI.mainMenu.aInsQuoteRD.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == CONFIG.fmtDQuoteClose - nwGUI.docEditor.clear() + mainMenu.aInsQuoteRD.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == CONFIG.fmtDQuoteClose + docEditor.clear() - nwGUI.mainMenu.aInsMSApos.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_MAPOS - nwGUI.docEditor.clear() + mainMenu.aInsMSApos.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_MAPOS + docEditor.clear() - nwGUI.mainMenu.aInsEllipsis.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_HELLIP - nwGUI.docEditor.clear() + mainMenu.aInsEllipsis.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_HELLIP + docEditor.clear() - nwGUI.mainMenu.aInsPrime.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_PRIME - nwGUI.docEditor.clear() + mainMenu.aInsPrime.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_PRIME + docEditor.clear() - nwGUI.mainMenu.aInsDPrime.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_DPRIME - nwGUI.docEditor.clear() + mainMenu.aInsDPrime.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_DPRIME + docEditor.clear() - nwGUI.mainMenu.aInsBullet.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_BULL - nwGUI.docEditor.clear() + mainMenu.aInsBullet.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_BULL + docEditor.clear() - nwGUI.mainMenu.aInsHyBull.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_HYBULL - nwGUI.docEditor.clear() + mainMenu.aInsHyBull.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_HYBULL + docEditor.clear() - nwGUI.mainMenu.aInsFlower.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_FLOWER - nwGUI.docEditor.clear() + mainMenu.aInsFlower.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_FLOWER + docEditor.clear() - nwGUI.mainMenu.aInsPerMille.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_PERMIL - nwGUI.docEditor.clear() + mainMenu.aInsPerMille.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_PERMIL + docEditor.clear() - nwGUI.mainMenu.aInsDegree.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_DEGREE - nwGUI.docEditor.clear() + mainMenu.aInsDegree.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_DEGREE + docEditor.clear() - nwGUI.mainMenu.aInsMinus.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_MINUS - nwGUI.docEditor.clear() + mainMenu.aInsMinus.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_MINUS + docEditor.clear() - nwGUI.mainMenu.aInsTimes.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_TIMES - nwGUI.docEditor.clear() + mainMenu.aInsTimes.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_TIMES + docEditor.clear() - nwGUI.mainMenu.aInsDivide.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_DIVIDE - nwGUI.docEditor.clear() + mainMenu.aInsDivide.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_DIVIDE + docEditor.clear() - nwGUI.mainMenu.aInsNBSpace.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_NBSP - nwGUI.docEditor.clear() + mainMenu.aInsNBSpace.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_NBSP + docEditor.clear() - nwGUI.mainMenu.aInsThinSpace.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_THSP - nwGUI.docEditor.clear() + mainMenu.aInsThinSpace.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_THSP + docEditor.clear() - nwGUI.mainMenu.aInsThinNBSpace.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_THNBSP - nwGUI.docEditor.clear() + mainMenu.aInsThinNBSpace.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == nwUnicode.U_THNBSP + docEditor.clear() # Insert Keywords # =============== - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.TAG_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.TAG_KEY - - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.POV_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.POV_KEY - - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.FOCUS_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.FOCUS_KEY - - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.CHAR_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.CHAR_KEY - - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.PLOT_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.PLOT_KEY - - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.TIME_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.TIME_KEY - - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.WORLD_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.WORLD_KEY - - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.OBJECT_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.OBJECT_KEY - - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.ENTITY_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.ENTITY_KEY - - nwGUI.docEditor.setPlainText("Stuff") - nwGUI.mainMenu.mInsKWItems[nwKeyWords.CUSTOM_KEY][0].activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.CUSTOM_KEY + for action, key in zip(mainMenu.mInsKeywords.actions(), nwKeyWords.ALL_KEYS): + docEditor.setPlainText("Stuff") + action.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == f"Stuff\n{key}: " # Faulty Keyword Inserts - assert not nwGUI.docEditor.insertKeyWord("blabla") + assert not docEditor.insertKeyWord("blabla") with monkeypatch.context() as mp: mp.setattr(QTextBlock, "isValid", lambda *a, **k: False) - assert not nwGUI.docEditor.insertKeyWord(nwKeyWords.TAG_KEY) + assert not docEditor.insertKeyWord(nwKeyWords.TAG_KEY) - nwGUI.docEditor.clear() + # Insert Fields + # ============= + + for action, field in zip(mainMenu.mInsField.actions(), nwStats.ALL_FIELDS): + value = nwShortcode.FIELD_VALUE.format(field) + docEditor.setPlainText("Stuff ") + docEditor.setCursorPosition(6) + action.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == f"Stuff {value}" + + docEditor.clear() # Insert Special Comments # ======================= - nwGUI.docEditor.setPlainText("Stuff\n") - nwGUI.mainMenu.aInsSynopsis.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%Synopsis: \n" + docEditor.setPlainText("Stuff\n") + mainMenu.aInsSynopsis.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == "Stuff\n%Synopsis: \n" - nwGUI.docEditor.setPlainText("Stuff\n") - nwGUI.mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Stuff\n%Short: \n" + docEditor.setPlainText("Stuff\n") + mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == "Stuff\n%Short: \n" # Breaks and Vertical Space # ========================= - nwGUI.docEditor.setPlainText("### Stuff\n") - nwGUI.mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "[newpage]\n### Stuff\n" + docEditor.setPlainText("### Stuff\n") + mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == "[newpage]\n### Stuff\n" - nwGUI.docEditor.setPlainText("Line OneLine Two\n") - nwGUI.docEditor.setCursorPosition(8) - nwGUI.mainMenu.aInsLineBreak.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Line One[br]Line Two\n" + docEditor.setPlainText("Line OneLine Two\n") + docEditor.setCursorPosition(8) + mainMenu.aInsLineBreak.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == "Line One[br]Line Two\n" - nwGUI.docEditor.setPlainText("### Stuff\n") - nwGUI.mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n" + docEditor.setPlainText("### Stuff\n") + mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == "[vspace]\n### Stuff\n" - nwGUI.docEditor.setPlainText("### Stuff\n") - nwGUI.mainMenu.aInsVSpaceM.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "[vspace:2]\n### Stuff\n" + docEditor.setPlainText("### Stuff\n") + mainMenu.aInsVSpaceM.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == "[vspace:2]\n### Stuff\n" - nwGUI.docEditor.clear() + docEditor.clear() # Insert Text from File # ===================== @@ -600,23 +579,23 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd # Open the document from before, and add some text to it nwGUI.openDocument(C.hSceneDoc) - nwGUI.docEditor.setPlainText("Bar") - assert nwGUI.docEditor.getText() == "Bar" + docEditor.setPlainText("Bar") + assert docEditor.getText() == "Bar" # The document isn't empty, so the message box should pop with monkeypatch.context() as mp: mp.setattr(QMessageBox, "result", lambda *a, **k: QMessageBox.StandardButton.No) assert not nwGUI.importDocument() - assert nwGUI.docEditor.getText() == "Bar" + assert docEditor.getText() == "Bar" # Finally, accept the replaced text, this time we use the menu entry to trigger it - nwGUI.mainMenu.aImportFile.activate(QAction.ActionEvent.Trigger) - assert nwGUI.docEditor.getText() == "Foo" + mainMenu.aImportFile.activate(QAction.ActionEvent.Trigger) + assert docEditor.getText() == "Foo" # Reveal File Location # ==================== - nwGUI.mainMenu.aFileDetails.activate(QAction.ActionEvent.Trigger) + mainMenu.aFileDetails.activate(QAction.ActionEvent.Trigger) path = str(projPath / "content" / "000000000000f.nwd") assert SHARED.lastAlert.endswith(f"File Location: {path}") From 041183f91420e5adaf7b2125b0f2a5923d8fcc1b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:50:30 +0100 Subject: [PATCH 03/11] Add processing of fields in the Tokenizer --- novelwriter/constants.py | 1 + novelwriter/formats/shared.py | 3 ++- novelwriter/formats/tokenizer.py | 1 + sample/content/53b69b83cdafc.nwd | 6 ++++-- sample/nwProject.nwx | 12 +++++------ tests/test_formats/test_fmt_tokenizer.py | 26 ++++++++++++++++++++++++ 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 83c22ac6..af232328 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -89,6 +89,7 @@ class nwShortcode: BREAK = "[br]" FOOTNOTE_B = "[footnote:" + FIELD_B = "[field:" COMMENT_STYLES = { nwComment.FOOTNOTE: "[footnote:{0}]", diff --git a/novelwriter/formats/shared.py b/novelwriter/formats/shared.py index 170ee890..119eee9a 100644 --- a/novelwriter/formats/shared.py +++ b/novelwriter/formats/shared.py @@ -92,7 +92,8 @@ class TextFmt(IntEnum): HRF_B = 21 # Begin href link HRF_E = 22 # End href link FNOTE = 23 # Footnote marker - STRIP = 24 # Strip the format code + FIELD = 24 # Data field + STRIP = 25 # Strip the format code class BlockTyp(IntEnum): diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 09e8e2ab..feb77ffd 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -193,6 +193,7 @@ class Tokenizer(ABC): } self._shortCodeVals = { nwShortcode.FOOTNOTE_B: TextFmt.FNOTE, + nwShortcode.FIELD_B: TextFmt.FIELD, } # Dialogue diff --git a/sample/content/53b69b83cdafc.nwd b/sample/content/53b69b83cdafc.nwd index f793413a..66e5a513 100644 --- a/sample/content/53b69b83cdafc.nwd +++ b/sample/content/53b69b83cdafc.nwd @@ -1,8 +1,8 @@ %%~name: Title Page %%~path: 7031beac91f75/53b69b83cdafc %%~kind: NOVEL/DOCUMENT -%%~hash: 4072adb6d21ff877577f033f19714d9bd01396f3 -%%~date: Unknown/2024-10-24 16:25:44 +%%~hash: 45ec619c9fd1b1185bc35b38d28cda13b05f0a88 +%%~date: Unknown/2024-10-28 16:42:26 Jane Smith[br] 42 Main Street[br] @@ -16,3 +16,5 @@ Jane Smith[br] >> This is the title page. << >> It should be the first document of the project. << + +>> Word Count: [field:textWords] << \ No newline at end of file diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index b8252bfc..1308d3b1 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -9,8 +9,8 @@ en_GB None - 636b6aa9b697b - 636b6aa9b697b + 53b69b83cdafc + 53b69b83cdafc 7031beac91f75 7031beac91f75 @@ -36,13 +36,13 @@ Main - + Novel - + Title Page diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 8d01445e..874f3211 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -1241,6 +1241,32 @@ def testFmtToken_LineBreak(mockGUI): ] +@pytest.mark.core +def testFmtToken_ShortcodeValue(mockGUI): + """Test processing of shortcodes with values.""" + project = NWProject() + tokens = BareTokenizer(project) + tokens._handle = TMH + + # Footnote + tokens._text = "Hello World[footnote:abcd] to you!" + tokens.tokenizeText() + assert tokens._blocks == [( + BlockTyp.TEXT, "", "Hello World to you!", [ + (11, TextFmt.FNOTE, f"{TMH}:abcd"), + ], BlockFmt.NONE + )] + + # Field + tokens._text = "Hello World: [field:abcd] times!" + tokens.tokenizeText() + assert tokens._blocks == [( + BlockTyp.TEXT, "", "Hello World: times!", [ + (13, TextFmt.FIELD, f"{TMH}:abcd"), + ], BlockFmt.NONE + )] + + @pytest.mark.core def testFmtToken_Dialogue(mockGUI): """Test the tokenization of dialogue in the Tokenizer class.""" From f92521c93a2313bc8aacd8c4f959e691c348d565 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:30:34 +0100 Subject: [PATCH 04/11] Process field values in QTextDocuments --- novelwriter/core/docbuild.py | 9 ++++--- novelwriter/formats/tohtml.py | 2 +- novelwriter/formats/tokenizer.py | 4 +++ novelwriter/formats/tomarkdown.py | 2 +- novelwriter/formats/toqdoc.py | 30 +++++++++++++++++------ novelwriter/formats/toraw.py | 4 +++ novelwriter/gui/docviewer.py | 2 +- novelwriter/tools/manuscript.py | 2 +- sample/nwProject.nwx | 8 +++--- tests/test_formats/test_fmt_tohtml.py | 2 +- tests/test_formats/test_fmt_tokenizer.py | 3 +++ tests/test_formats/test_fmt_tomarkdown.py | 2 +- tests/test_formats/test_fmt_toqdoc.py | 2 +- 13 files changed, 50 insertions(+), 22 deletions(-) diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index a2417e8f..0e91b15b 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -132,7 +132,7 @@ class NWBuildDocument: yield from self._iterBuild(makeObj, filtered) - makeObj.appendFootnotes() + makeObj.closeDocument() self._error = None self._cache = makeObj @@ -164,7 +164,7 @@ class NWBuildDocument: yield from self._iterBuild(makeObj, filtered) - makeObj.appendFootnotes() + makeObj.closeDocument() if not self._build.getBool("html.preserveTabs"): makeObj.replaceTabs() @@ -174,7 +174,7 @@ class NWBuildDocument: yield from self._iterBuild(makeObj, filtered) - makeObj.appendFootnotes() + makeObj.closeDocument() if self._build.getBool("format.replaceTabs"): makeObj.replaceTabs(nSpaces=4, spaceChar=" ") @@ -184,6 +184,7 @@ class NWBuildDocument: yield from self._iterBuild(makeObj, filtered) + makeObj.closeDocument() if self._build.getBool("format.replaceTabs"): makeObj.replaceTabs(nSpaces=4, spaceChar=" ") @@ -203,7 +204,7 @@ class NWBuildDocument: yield from self._iterBuild(makeObj, filtered) - makeObj.appendFootnotes() + makeObj.closeDocument() else: logger.error("Unsupported document format") diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py index 484a48ee..f7634a61 100644 --- a/novelwriter/formats/tohtml.py +++ b/novelwriter/formats/tohtml.py @@ -249,7 +249,7 @@ class ToHtml(Tokenizer): return - def appendFootnotes(self) -> None: + def closeDocument(self) -> None: """Append the footnotes in the buffer.""" if self._usedNotes: footnotes = self._localLookup("Footnotes") diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index feb77ffd..64017e7e 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -428,6 +428,10 @@ class Tokenizer(ABC): def doConvert(self) -> None: raise NotImplementedError + @abstractmethod + def closeDocument(self) -> None: + raise NotImplementedError + @abstractmethod def saveDocument(self, path: Path) -> None: raise NotImplementedError diff --git a/novelwriter/formats/tomarkdown.py b/novelwriter/formats/tomarkdown.py index 61111091..ff1aa48e 100644 --- a/novelwriter/formats/tomarkdown.py +++ b/novelwriter/formats/tomarkdown.py @@ -149,7 +149,7 @@ class ToMarkdown(Tokenizer): return - def appendFootnotes(self) -> None: + def closeDocument(self) -> None: """Append the footnotes in the buffer.""" if self._usedNotes: tags = EXT_MD if self._extended else STD_MD diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 84745c58..6ef06d7d 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -40,8 +40,8 @@ from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt from novelwriter.formats.tokenizer import HEADINGS, Tokenizer from novelwriter.types import ( QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight, - QtPageBreakAfter, QtPageBreakBefore, QtTransparent, QtVAlignNormal, - QtVAlignSub, QtVAlignSuper + QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakBefore, + QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper ) logger = logging.getLogger(__name__) @@ -70,6 +70,7 @@ class ToQTextDocument(Tokenizer): self._document.setDocumentMargin(0) self._usedNotes: dict[str, int] = {} + self._usedFields: list[tuple[int, str]] = [] self._init = False self._bold = QFont.Weight.Bold @@ -246,11 +247,21 @@ class ToQTextDocument(Tokenizer): return - def appendFootnotes(self) -> None: - """Append the footnotes in the buffer.""" - if self._usedNotes: - self._document.blockSignals(True) + def closeDocument(self) -> None: + """Run close document tasks.""" + self._document.blockSignals(True) + # Replace fields if there are stats available + if self._usedFields and self._counts: + cursor = QTextCursor(self._document) + for pos, field in reversed(self._usedFields): + if (value := self._counts.get(field)) is not None: + cursor.setPosition(pos, QtMoveAnchor) + cursor.setPosition(pos + 1, QtKeepAnchor) + cursor.insertText(f"{value:n}") + + # Add footnotes + if self._usedNotes: cursor = QTextCursor(self._document) cursor.movePosition(QTextCursor.MoveOperation.End) @@ -268,7 +279,7 @@ class ToQTextDocument(Tokenizer): cursor.insertText(f"{index}. ", cFmt) self._insertFragments(*content, cursor, self._charFmt) - self._document.blockSignals(False) + self._document.blockSignals(False) return @@ -361,6 +372,11 @@ class ToQTextDocument(Tokenizer): cursor.insertText(f"[{index}]", xFmt) else: cursor.insertText("[ERR]", cFmt) + elif fmt == TextFmt.FIELD: + if field := data.partition(":")[2]: + self._usedFields.append((cursor.position(), field)) + cursor.insertText("0", cFmt) + pass # Move pos for next pass start = pos diff --git a/novelwriter/formats/toraw.py b/novelwriter/formats/toraw.py index 8e1ed47e..9a410f2f 100644 --- a/novelwriter/formats/toraw.py +++ b/novelwriter/formats/toraw.py @@ -52,6 +52,10 @@ class ToRaw(Tokenizer): """No conversion to perform.""" return + def closeDocument(self) -> None: + """Nothing to close.""" + return + def saveDocument(self, path: Path) -> None: """Save the raw text to a plain text file.""" if path.suffix.lower() == ".json": diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 13d641fa..a304b12e 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -229,7 +229,7 @@ class GuiDocViewer(QTextBrowser): qDoc.doPreProcessing() qDoc.tokenizeText() qDoc.doConvert() - qDoc.appendFootnotes() + qDoc.closeDocument() except Exception: logger.error("Failed to generate preview for document with handle '%s'", tHandle) logException() diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index af8f5a3b..dfcb4d3d 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -955,7 +955,7 @@ class _StatsWidget(QWidget): # Maximal self.maxTotalWords.setText("{0:n}".format(data.get(nwStats.WORDS_ALL, 0))) self.maxHeadWords.setText("{0:n}".format(data.get(nwStats.WORDS_TITLE, 0))) - self.maxTextWords.setText("{0:n}".format(data.get(nwStats.WORDS_TITLE, 0))) + self.maxTextWords.setText("{0:n}".format(data.get(nwStats.WORDS_TEXT, 0))) self.maxTitleCount.setText("{0:n}".format(data.get(nwStats.TITLES, 0))) self.maxParCount.setText("{0:n}".format(data.get(nwStats.PARAGRAPHS, 0))) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 1308d3b1..aaca21b8 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -9,8 +9,8 @@ en_GB None - 53b69b83cdafc - 53b69b83cdafc + 636b6aa9b697b + 636b6aa9b697b 7031beac91f75 7031beac91f75 diff --git a/tests/test_formats/test_fmt_tohtml.py b/tests/test_formats/test_fmt_tohtml.py index ac5abbec..d9257738 100644 --- a/tests/test_formats/test_fmt_tohtml.py +++ b/tests/test_formats/test_fmt_tohtml.py @@ -315,7 +315,7 @@ def testFmtToHtml_ConvertParagraphs(mockGUI): "or twoERR footnotes.

\n" ) - html.appendFootnotes() + html.closeDocument() assert html._pages[-2] == ( "

Text with one1 " "or twoERR footnotes.

\n" diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 874f3211..4b3178b8 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -44,6 +44,9 @@ class BareTokenizer(Tokenizer): def doConvert(self): super().doConvert() # type: ignore (deliberate check) + def closeDocument(self): + super().doConvert() # type: ignore (deliberate check) + def saveDocument(self, path) -> None: super().saveDocument(path) # type: ignore (deliberate check) diff --git a/tests/test_formats/test_fmt_tomarkdown.py b/tests/test_formats/test_fmt_tomarkdown.py index e94f4b72..fc53eb7e 100644 --- a/tests/test_formats/test_fmt_tomarkdown.py +++ b/tests/test_formats/test_fmt_tomarkdown.py @@ -205,7 +205,7 @@ def testFmtToMarkdown_ConvertParagraphs(mockGUI): md.doConvert() assert md._pages[-1] == "Text with one[1] or two[ERR] footnotes.\n\n" - md.appendFootnotes() + md.closeDocument() assert md._pages[-2] == ( "Text with one[1] or two[ERR] footnotes.\n\n" ) diff --git a/tests/test_formats/test_fmt_toqdoc.py b/tests/test_formats/test_fmt_toqdoc.py index 1914c82e..46ff0b85 100644 --- a/tests/test_formats/test_fmt_toqdoc.py +++ b/tests/test_formats/test_fmt_toqdoc.py @@ -594,7 +594,7 @@ def testFmtToQTextDocument_Footnotes(mockGUI): ) doc.tokenizeText() doc.doConvert() - doc.appendFootnotes() + doc.closeDocument() assert doc.document.blockCount() == 4 # 0: Scene From 42f904f1eab4e30b2bae44128d74f66dacbc03aa Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:17:25 +0100 Subject: [PATCH 05/11] Add field value insertion to all formats --- novelwriter/core/docbuild.py | 17 ----------------- novelwriter/formats/todocx.py | 18 ++++++++++++++++++ novelwriter/formats/tohtml.py | 15 ++++++++++++++- novelwriter/formats/tomarkdown.py | 15 ++++++++++++++- novelwriter/formats/toodt.py | 12 ++++++++++++ 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 0e91b15b..ba3e469d 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -127,16 +127,11 @@ class NWBuildDocument: makeObj = ToQTextDocument(self._project) filtered = self._setupBuild(makeObj) makeObj.initDocument() - self._outline = True - yield from self._iterBuild(makeObj, filtered) - makeObj.closeDocument() - self._error = None self._cache = makeObj - return def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]: @@ -152,18 +147,14 @@ class NWBuildDocument: makeObj = ToOdt(self._project, bFormat == nwBuildFmt.FODT) filtered = self._setupBuild(makeObj) makeObj.initDocument() - yield from self._iterBuild(makeObj, filtered) - makeObj.closeDocument() elif bFormat in (nwBuildFmt.HTML, nwBuildFmt.J_HTML): makeObj = ToHtml(self._project) filtered = self._setupBuild(makeObj) makeObj.initDocument() - yield from self._iterBuild(makeObj, filtered) - makeObj.closeDocument() if not self._build.getBool("html.preserveTabs"): makeObj.replaceTabs() @@ -171,9 +162,7 @@ class NWBuildDocument: elif bFormat in (nwBuildFmt.STD_MD, nwBuildFmt.EXT_MD): makeObj = ToMarkdown(self._project, bFormat == nwBuildFmt.EXT_MD) filtered = self._setupBuild(makeObj) - yield from self._iterBuild(makeObj, filtered) - makeObj.closeDocument() if self._build.getBool("format.replaceTabs"): makeObj.replaceTabs(nSpaces=4, spaceChar=" ") @@ -181,9 +170,7 @@ class NWBuildDocument: elif bFormat in (nwBuildFmt.NWD, nwBuildFmt.J_NWD): makeObj = ToRaw(self._project) filtered = self._setupBuild(makeObj) - yield from self._iterBuild(makeObj, filtered) - makeObj.closeDocument() if self._build.getBool("format.replaceTabs"): makeObj.replaceTabs(nSpaces=4, spaceChar=" ") @@ -192,18 +179,14 @@ class NWBuildDocument: makeObj = ToDocX(self._project) filtered = self._setupBuild(makeObj) makeObj.initDocument() - yield from self._iterBuild(makeObj, filtered) - makeObj.closeDocument() elif bFormat == nwBuildFmt.PDF: makeObj = ToQTextDocument(self._project) filtered = self._setupBuild(makeObj) makeObj.initDocument() - yield from self._iterBuild(makeObj, filtered) - makeObj.closeDocument() else: diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 2f37d5cd..4f426453 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -192,6 +192,7 @@ class ToDocX(Tokenizer): self._files: dict[str, DocXXmlFile] = {} self._styles: dict[str, DocXParStyle] = {} self._usedNotes: dict[str, int] = {} + self._usedFields: list[tuple[ET.Element, str]] = [] return @@ -440,6 +441,8 @@ class ToDocX(Tokenizer): fLink = "" elif fFmt == TextFmt.FNOTE: xNode = self._generateFootnote(fData) + elif fFmt == TextFmt.FIELD: + xNode = self._generateField(fData) elif fFmt == TextFmt.STRIP: pass @@ -510,6 +513,15 @@ class ToDocX(Tokenizer): return xR return None + def _generateField(self, key: str) -> ET.Element | None: + """Generate a data field XML object.""" + if key and (field := key.partition(":")[2]): + xR = ET.Element(_wTag("r")) + xT = xmlSubElem(xR, _wTag("t"), "0") + self._usedFields.append((xT, field)) + return xR + return None + def _generateStyles(self) -> None: """Generate usable styles.""" styles: list[DocXParStyle] = [] @@ -920,6 +932,12 @@ class ToDocX(Tokenizer): pars.append(par) + # Replace fields if there are stats available + if self._usedFields and self._counts: + for xField, field in self._usedFields: + if (value := self._counts.get(field)) is not None: + xField.text = f"{value:n}" + # Write Paragraphs for par in pars: par.toXml(xBody) diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py index f7634a61..e27a96dc 100644 --- a/novelwriter/formats/tohtml.py +++ b/novelwriter/formats/tohtml.py @@ -84,6 +84,7 @@ class ToHtml(Tokenizer): self._trMap = {} self._cssStyles = True self._usedNotes: dict[str, int] = {} + self._usedFields: list[tuple[int, str]] = [] self.setReplaceUnicode(False) return @@ -250,7 +251,15 @@ class ToHtml(Tokenizer): return def closeDocument(self) -> None: - """Append the footnotes in the buffer.""" + """Run close document tasks.""" + # Replace fields if there are stats available + if self._usedFields and self._counts: + pages = len(self._pages) + for doc, field in self._usedFields: + if doc >= 0 and doc < pages and (value := self._counts.get(field)) is not None: + self._pages[doc] = self._pages[doc].replace(f"{{{{{field}}}}}", f"{value:n}") + + # Add footnotes if self._usedNotes: footnotes = self._localLookup("Footnotes") @@ -416,6 +425,10 @@ class ToHtml(Tokenizer): tags.append((pos, f"{index}")) else: tags.append((pos, "ERR")) + elif fmt == TextFmt.FIELD: + if field := data.partition(":")[2]: + self._usedFields.append((len(self._pages), field)) + tags.append((pos, f"{{{{{field}}}}}")) # Check all format types and close any tag that is still open. This # ensures that unclosed tags don't spill over to the next paragraph. diff --git a/novelwriter/formats/tomarkdown.py b/novelwriter/formats/tomarkdown.py index ff1aa48e..1db36152 100644 --- a/novelwriter/formats/tomarkdown.py +++ b/novelwriter/formats/tomarkdown.py @@ -86,6 +86,7 @@ class ToMarkdown(Tokenizer): super().__init__(project) self._extended = extended self._usedNotes: dict[str, int] = {} + self._usedFields: list[tuple[int, str]] = [] return ## @@ -150,7 +151,15 @@ class ToMarkdown(Tokenizer): return def closeDocument(self) -> None: - """Append the footnotes in the buffer.""" + """Run close document tasks.""" + # Replace fields if there are stats available + if self._usedFields and self._counts: + pages = len(self._pages) + for doc, field in self._usedFields: + if doc >= 0 and doc < pages and (value := self._counts.get(field)) is not None: + self._pages[doc] = self._pages[doc].replace(f"{{{{{field}}}}}", f"{value:n}") + + # Add footnotes if self._usedNotes: tags = EXT_MD if self._extended else STD_MD footnotes = self._localLookup("Footnotes") @@ -196,6 +205,10 @@ class ToMarkdown(Tokenizer): md = f"[{index}]" else: md = "[ERR]" + elif fmt == TextFmt.FIELD: + if field := data.partition(":")[2]: + self._usedFields.append((len(self._pages), field)) + md = f"{{{{{field}}}}}" else: md = tags.get(fmt, "") temp = f"{temp[:pos]}{md}{temp[pos:]}" diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index afb15203..0845b14a 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -630,6 +630,8 @@ class ToOdt(Tokenizer): fLink = "" elif fFmt == TextFmt.FNOTE: xNode = self._generateFootnote(fData) + elif fFmt == TextFmt.FIELD: + xNode = self._generateField(fData) elif fFmt == TextFmt.STRIP: pass @@ -725,6 +727,16 @@ class ToOdt(Tokenizer): return xNote return None + def _generateField(self, key: str) -> ET.Element | None: + """Generate a data field XML object.""" + if key and (field := key.partition(":")[2]): + xField = ET.Element(_mkTag("text", "user-field-get"), attrib={ + _mkTag("text", "name"): f"Manuscript{field[:1].upper()}{field[1:]}", + }) + xField.text = "0" + return xField + return None + def _emToCm(self, value: float) -> str: """Converts an em value to centimetres.""" return f"{value*2.54/72*self._fontSize:.3f}cm" From c6a22e2d96e1f30b98c2b6f3086511aecd52d407 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:46:06 +0100 Subject: [PATCH 06/11] Add stats variables to Lipsum test project --- tests/lipsum/content/7a992350f3eb6.nwd | 8 ++++-- tests/lipsum/nwProject.nwx | 12 ++++----- .../coreIndex_LoadSave_tagsIndex.json | 2 +- .../reference/fmtToDocX_SaveDocument_app.xml | 12 ++++----- .../reference/fmtToDocX_SaveDocument_core.xml | 6 ++--- .../fmtToDocX_SaveDocument_document.xml | 26 +++++++++++++++++++ .../fmtToDocX_SaveDocument_settings.xml | 14 +++++----- .../reference/guiOutline_Content_outline.csv | 2 +- ...dDocBuild_Extended_Markdown_Lorem_Ipsum.md | 4 +++ .../mBuildDocBuild_HTML5_Lorem_Ipsum.htm | 2 ++ .../mBuildDocBuild_HTML5_Lorem_Ipsum.json | 6 +++-- .../mBuildDocBuild_NWD_Lorem_Ipsum.json | 8 ++++-- .../mBuildDocBuild_NWD_Lorem_Ipsum.txt | 4 +++ ...uildDocBuild_OpenDocument_Lorem_Ipsum.fodt | 26 ++++++++++--------- ...dDocBuild_Standard_Markdown_Lorem_Ipsum.md | 4 +++ tests/test_gui/test_gui_doceditor.py | 8 +++--- tests/test_gui/test_gui_outline.py | 6 ++--- tests/test_tools/test_tools_noveldetails.py | 12 ++++----- 18 files changed, 107 insertions(+), 55 deletions(-) diff --git a/tests/lipsum/content/7a992350f3eb6.nwd b/tests/lipsum/content/7a992350f3eb6.nwd index f36d5e4d..c57db169 100644 --- a/tests/lipsum/content/7a992350f3eb6.nwd +++ b/tests/lipsum/content/7a992350f3eb6.nwd @@ -1,12 +1,16 @@ %%~name: Lorem Ipsum %%~path: b3643d0f92e32/7a992350f3eb6 %%~kind: NOVEL/DOCUMENT -%%~hash: 8efda028000b70be0d7dbe9647b6026082ef05c9 -%%~date: Unknown/Unknown +%%~hash: 370be3057fb6c0e0b5e4858915a37da18ffc7675 +%%~date: Unknown/2024-10-28 18:18:37 #! Lorem Ipsum >> **By lipsum.com** << +>> Word Count: [field:allWords] << + +>> Character Count: [field:allChars] << + >> “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” << >> “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” << diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index 42dddbe6..8da7d6cc 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Lorem Ipsum lipsum.com @@ -9,7 +9,7 @@ en_GB None - 88d59a277361b + 7a992350f3eb6 None b3643d0f92e32 None @@ -31,17 +31,17 @@ Main - + Novel - + Lorem Ipsum - + Front Matter diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index 9a6c8e6e..4412cd18 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -7,7 +7,7 @@ "novelWriter.itemIndex": { "7a992350f3eb6": { "headings": { - "T0001": {"level": "H1", "title": "Lorem Ipsum", "line": 1, "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""} + "T0001": {"level": "H1", "title": "Lorem Ipsum", "line": 1, "tag": "", "cCount": 259, "wCount": 44, "pCount": 5, "synopsis": ""} } }, "8c58a65414c23": { diff --git a/tests/reference/fmtToDocX_SaveDocument_app.xml b/tests/reference/fmtToDocX_SaveDocument_app.xml index 2b23457d..59d08d55 100644 --- a/tests/reference/fmtToDocX_SaveDocument_app.xml +++ b/tests/reference/fmtToDocX_SaveDocument_app.xml @@ -1,9 +1,9 @@ - 39 - novelWriter/2.6a2 - 4031 - 21271 - 24935 - 43 + 40 + novelWriter/2.6a3 + 4035 + 21296 + 24964 + 45 diff --git a/tests/reference/fmtToDocX_SaveDocument_core.xml b/tests/reference/fmtToDocX_SaveDocument_core.xml index 298cdff1..1e503ab2 100644 --- a/tests/reference/fmtToDocX_SaveDocument_core.xml +++ b/tests/reference/fmtToDocX_SaveDocument_core.xml @@ -1,10 +1,10 @@ - 2024-10-26T17:01:14 - 2024-10-26T17:01:14 + 2024-10-28T18:23:43 + 2024-10-28T18:23:43 lipsum.com Lorem Ipsum en-GB - 48 + 50 lipsum.com diff --git a/tests/reference/fmtToDocX_SaveDocument_document.xml b/tests/reference/fmtToDocX_SaveDocument_document.xml index a464186f..4d840bc5 100644 --- a/tests/reference/fmtToDocX_SaveDocument_document.xml +++ b/tests/reference/fmtToDocX_SaveDocument_document.xml @@ -23,6 +23,32 @@ By lipsum.com + + + + + + + + Word Count: + + + 4,035 + + + + + + + + + + Character Count: + + + 27,064 + + diff --git a/tests/reference/fmtToDocX_SaveDocument_settings.xml b/tests/reference/fmtToDocX_SaveDocument_settings.xml index 3d8dd1db..10913023 100644 --- a/tests/reference/fmtToDocX_SaveDocument_settings.xml +++ b/tests/reference/fmtToDocX_SaveDocument_settings.xml @@ -8,15 +8,15 @@ - - - + + + - - + + - - + + diff --git a/tests/reference/guiOutline_Content_outline.csv b/tests/reference/guiOutline_Content_outline.csv index 5a275769..15ca2c46 100644 --- a/tests/reference/guiOutline_Content_outline.csv +++ b/tests/reference/guiOutline_Content_outline.csv @@ -1,5 +1,5 @@ "Title","Document","Words","Pars","POV","Characters","Plot","Locations","Synopsis" -"Lorem Ipsum","Lorem Ipsum","40","3","","","","","" +"Lorem Ipsum","Lorem Ipsum","44","5","","","","","" "Prologue","Prologue","94","2","","","","","Explanation from the lipsum.com website." "Act One","Act One","6","1","","","","","" "Chapter One","Chapter One","67","1","Bod","","Main","Europe","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam." diff --git a/tests/reference/mBuildDocBuild_Extended_Markdown_Lorem_Ipsum.md b/tests/reference/mBuildDocBuild_Extended_Markdown_Lorem_Ipsum.md index 925a15c5..a9627afd 100644 --- a/tests/reference/mBuildDocBuild_Extended_Markdown_Lorem_Ipsum.md +++ b/tests/reference/mBuildDocBuild_Extended_Markdown_Lorem_Ipsum.md @@ -2,6 +2,10 @@ **By lipsum.com** +Word Count: 4,169 + +Character Count: 27,898 + “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm index c30f2ca4..3a6a9cd0 100644 --- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm +++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm @@ -21,6 +21,8 @@ h4 {margin-top: 1.53em; margin-bottom: 0.65em;}

Lorem Ipsum

By lipsum.com

+

Word Count: 4,169

+

Character Count: 27,898

“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”

“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”

Comment: Exctracted from the lipsum.com website.

diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json index 6e9df57b..b4a31f8d 100644 --- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json +++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json @@ -2,8 +2,8 @@ "meta": { "projectName": "Lorem Ipsum", "novelAuthor": "lipsum.com", - "buildTime": 1729877010, - "buildTimeStr": "2024-10-25 19:23:30" + "buildTime": 1730136403, + "buildTimeStr": "2024-10-28 18:26:43" }, "text": { "css": [ @@ -23,6 +23,8 @@ [ "

Lorem Ipsum

", "

By lipsum.com

", + "

Word Count: 4,169

", + "

Character Count: 27,898

", "

\u201cNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u2026\u201d

", "

\u201cThere is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain\u2026\u201d

" ], diff --git a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json index ce7f456b..e3b1ccee 100644 --- a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json +++ b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json @@ -2,8 +2,8 @@ "meta": { "projectName": "Lorem Ipsum", "novelAuthor": "lipsum.com", - "buildTime": 1729877010, - "buildTimeStr": "2024-10-25 19:23:30" + "buildTime": 1730136328, + "buildTimeStr": "2024-10-28 18:25:28" }, "text": { "nwd": [ @@ -12,6 +12,10 @@ "", ">> **By lipsum.com** <<", "", + ">> Word Count: [field:allWords] <<", + "", + ">> Character Count: [field:allChars] <<", + "", ">> \u201cNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u2026\u201d <<", "", ">> \u201cThere is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain\u2026\u201d <<" diff --git a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt index 424c373b..73d5d3d5 100644 --- a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt +++ b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt @@ -2,6 +2,10 @@ >> **By lipsum.com** << +>> Word Count: [field:allWords] << + +>> Character Count: [field:allChars] << + >> “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” << >> “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” << diff --git a/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt b/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt index 34e325b2..6299ba17 100644 --- a/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt +++ b/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt @@ -1,13 +1,13 @@ - 2024-10-25T19:19:43 - novelWriter/2.6a2 + 2024-10-28T18:27:00 + novelWriter/2.6a3 lipsum.com - 48 - P0DT0H39M34S + 50 + P0DT0H40M48S Lorem Ipsum - 2024-10-25T19:19:43 + 2024-10-28T18:27:00 lipsum.com @@ -151,19 +151,21 @@ - - - + + + - - + + - - + + Lorem Ipsum By lipsum.com + Word Count: 0 + Character Count: 0 “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” Comment: Exctracted from the lipsum.com website. diff --git a/tests/reference/mBuildDocBuild_Standard_Markdown_Lorem_Ipsum.md b/tests/reference/mBuildDocBuild_Standard_Markdown_Lorem_Ipsum.md index 925a15c5..a9627afd 100644 --- a/tests/reference/mBuildDocBuild_Standard_Markdown_Lorem_Ipsum.md +++ b/tests/reference/mBuildDocBuild_Standard_Markdown_Lorem_Ipsum.md @@ -2,6 +2,10 @@ **By lipsum.com** +Word Count: 4,169 + +Character Count: 27,898 + “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 2e0cac3f..3910e7b1 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -2167,15 +2167,15 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): docEditor._lastFind = None docEditor.replaceNext() assert docEditor.textCursor().selectedText() == "a" - assert docEditor.getCursorPosition() == 92 + assert docEditor.getCursorPosition() == 83 # Iterate through the rest - finds = [104, 123, 175, 197, 206, 211, 220, 238, 250, 250] - for i in range(10): + finds = [85, 105, 110, 141, 169, 181, 200, 252, 274, 283, 288, 297] + for i in range(len(finds)): docEditor.replaceNext() assert docEditor.textCursor().selectedText() == "a" assert docEditor.getCursorPosition() == finds[i] - assert docEditor._lastFind == (249, 250) + assert docEditor._lastFind == (296, 297) # Search for something that doesn't exist docSearch.searchBox.setText("x") diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 4f53eef7..3dc6b32b 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -241,9 +241,9 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPat assert outlineData.fileValue.text() == "Lorem Ipsum" assert outlineData.itemValue.text() == "Finished" - assert outlineData.cCValue.text() == "230" - assert outlineData.wCValue.text() == "40" - assert outlineData.pCValue.text() == "3" + assert outlineData.cCValue.text() == "259" + assert outlineData.wCValue.text() == "44" + assert outlineData.pCValue.text() == "5" # Scene One selItem = outlineTree.topLevelItem(4) diff --git a/tests/test_tools/test_tools_noveldetails.py b/tests/test_tools/test_tools_noveldetails.py index c15b43bb..98cbf22e 100644 --- a/tests/test_tools/test_tools_noveldetails.py +++ b/tests/test_tools/test_tools_noveldetails.py @@ -60,15 +60,15 @@ def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText): # Check project data assert overview.projName.text() == "Lorem Ipsum" - assert overview.projWords.text() == f"{4378:n}" - assert overview.projNovels.text() == f"{3640:n}" + assert overview.projWords.text() == f"{4382:n}" + assert overview.projNovels.text() == f"{3644:n}" assert overview.projNotes.text() == f"{738:n}" assert overview.projRevisions.text() != "" assert overview.projEditTime.text() != "" # Check novel data for "Novel" assert overview.novelName.text() == "Novel" - assert overview.novelWords.text() == f"{3002:n}" + assert overview.novelWords.text() == f"{3006:n}" assert overview.novelChapters.text() == f"{3:n}" assert overview.novelScenes.text() == f"{5:n}" @@ -87,7 +87,7 @@ def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText): contents = details.contentsPage # Check defaults - words = [f"{v:n}" for v in [40, 176, 94, 6, 1071, 1615, 0]] + words = [f"{v:n}" for v in [44, 176, 94, 6, 1071, 1615, 0]] pages = [f"{v:n}" for v in [2, 2, 2, 2, 4, 6, 0]] page = [f"{v:n}" for v in [1, 3, 5, 7, 9, 13, 19]] for i in range(6): @@ -100,7 +100,7 @@ def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText): # Change Settings contents.poValue.setValue(7) contents.wpValue.setValue(50) - words = [f"{v:n}" for v in [40, 176, 94, 6, 1071, 1615, 0]] + words = [f"{v:n}" for v in [44, 176, 94, 6, 1071, 1615, 0]] pages = [f"{v:n}" for v in [2, 4, 2, 2, 22, 34, 0]] page = ["i", "iii"] + [f"{v:n}" for v in [1, 3, 5, 27, 61]] for i in range(6): @@ -114,7 +114,7 @@ def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText): contents.dblValue.setChecked(False) contents.poValue.setValue(0) contents.wpValue.setValue(100) - words = [f"{v:n}" for v in [40, 176, 94, 6, 1071, 1615, 0]] + words = [f"{v:n}" for v in [44, 176, 94, 6, 1071, 1615, 0]] pages = [f"{v:n}" for v in [1, 2, 1, 1, 11, 17, 0]] page = [f"{v:n}" for v in [1, 2, 4, 5, 6, 17, 34]] for i in range(6): From d987246062960a6c99d3f664aa863d1634de48f7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:53:25 +0100 Subject: [PATCH 07/11] Use Qt for int locale --- novelwriter/config.py | 4 +++ novelwriter/formats/todocx.py | 4 +-- novelwriter/formats/tohtml.py | 5 ++- novelwriter/formats/tomarkdown.py | 5 ++- novelwriter/formats/toqdoc.py | 3 +- tests/test_core/test_core_docbuild.py | 48 +++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 5 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index d122a0b1..fb2ff9d3 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -469,6 +469,10 @@ class Config: """Return a localised datetime format.""" return self._dLocale.toString(value, self._dShortDateTime) + def localInt(self, value: int) -> str: + """Return a localised integer.""" + return self._dLocale.toString(value) + def listLanguages(self, lngSet: int) -> list[tuple[str, str]]: """List localisation files in the i18n folder. The default GUI language is British English (en_GB). diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 4f426453..55cdca21 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -36,7 +36,7 @@ from zipfile import ZIP_DEFLATED, ZipFile from PyQt5.QtCore import QMarginsF, QSizeF from PyQt5.QtGui import QColor -from novelwriter import __version__ +from novelwriter import CONFIG, __version__ from novelwriter.common import firstFloat, xmlSubElem from novelwriter.constants import nwHeadFmt, nwStyles from novelwriter.core.project import NWProject @@ -936,7 +936,7 @@ class ToDocX(Tokenizer): if self._usedFields and self._counts: for xField, field in self._usedFields: if (value := self._counts.get(field)) is not None: - xField.text = f"{value:n}" + xField.text = CONFIG.localInt(value) # Write Paragraphs for par in pars: diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py index e27a96dc..ca89e3fe 100644 --- a/novelwriter/formats/tohtml.py +++ b/novelwriter/formats/tohtml.py @@ -29,6 +29,7 @@ import logging from pathlib import Path from time import time +from novelwriter import CONFIG from novelwriter.common import formatTimeStamp from novelwriter.constants import nwHtmlUnicode from novelwriter.core.project import NWProject @@ -257,7 +258,9 @@ class ToHtml(Tokenizer): pages = len(self._pages) for doc, field in self._usedFields: if doc >= 0 and doc < pages and (value := self._counts.get(field)) is not None: - self._pages[doc] = self._pages[doc].replace(f"{{{{{field}}}}}", f"{value:n}") + self._pages[doc] = self._pages[doc].replace( + f"{{{{{field}}}}}", CONFIG.localInt(value) + ) # Add footnotes if self._usedNotes: diff --git a/novelwriter/formats/tomarkdown.py b/novelwriter/formats/tomarkdown.py index 1db36152..d7981249 100644 --- a/novelwriter/formats/tomarkdown.py +++ b/novelwriter/formats/tomarkdown.py @@ -27,6 +27,7 @@ import logging from pathlib import Path +from novelwriter import CONFIG from novelwriter.constants import nwUnicode from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt @@ -157,7 +158,9 @@ class ToMarkdown(Tokenizer): pages = len(self._pages) for doc, field in self._usedFields: if doc >= 0 and doc < pages and (value := self._counts.get(field)) is not None: - self._pages[doc] = self._pages[doc].replace(f"{{{{{field}}}}}", f"{value:n}") + self._pages[doc] = self._pages[doc].replace( + f"{{{{{field}}}}}", CONFIG.localInt(value) + ) # Add footnotes if self._usedNotes: diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 6ef06d7d..0bb32d4f 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -34,6 +34,7 @@ from PyQt5.QtGui import ( ) from PyQt5.QtPrintSupport import QPrinter +from novelwriter import CONFIG from novelwriter.constants import nwStyles, nwUnicode from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt @@ -258,7 +259,7 @@ class ToQTextDocument(Tokenizer): if (value := self._counts.get(field)) is not None: cursor.setPosition(pos, QtMoveAnchor) cursor.setPosition(pos + 1, QtKeepAnchor) - cursor.insertText(f"{value:n}") + cursor.insertText(CONFIG.localInt(value)) # Add footnotes if self._usedNotes: diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py index 7871f83f..713aab50 100644 --- a/tests/test_core/test_core_docbuild.py +++ b/tests/test_core/test_core_docbuild.py @@ -21,6 +21,7 @@ along with this program. If not, see . from __future__ import annotations import json +import zipfile from pathlib import Path from shutil import copyfile @@ -130,6 +131,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP assert error == [] assert docFile.is_file() + assert zipfile.is_zipfile(docFile) # Check Error Handling # ==================== @@ -318,6 +320,52 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths assert not docFile.is_file() +@pytest.mark.core +def testCoreDocBuild_DocX(monkeypatch, mockGUI, prjLipsum, fncPath): + """Test building a Word manuscript.""" + project = NWProject() + project.openProject(prjLipsum) + + build = BuildSettings() + build.unpack(BUILD_CONF) + + docBuild = NWBuildDocument(project, build) + docBuild.queueAll() + + assert len(docBuild) == 21 + + # Check Build + # =========== + + docFile = fncPath / "Lorem Ipsum.docx" + + count = 0 + error = [] + for _, success in docBuild.iterBuildDocument(docFile, nwBuildFmt.DOCX): + count += 1 if success else 0 + if docBuild.error: + error.append(docBuild.error) + + assert count == 19 + assert error == [] + + assert docFile.is_file() + assert zipfile.is_zipfile(docFile) + + # Check Error Handling + # ==================== + + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + + docFile = fncPath / "Lorem Ipsum Err.fodt" + for _ in docBuild.iterBuildDocument(docFile, nwBuildFmt.FODT): + pass + + assert docBuild.error == "OSError: Mock OSError" + assert not docFile.is_file() + + @pytest.mark.core def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): """Test building a NWD manuscript.""" From 9bc1561e01d6d430125140e39f703e107fac0cb8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:22:02 +0100 Subject: [PATCH 08/11] Change to use document locale for integers --- novelwriter/config.py | 8 ++++---- novelwriter/core/docbuild.py | 3 ++- novelwriter/formats/todocx.py | 15 ++++----------- novelwriter/formats/tohtml.py | 3 +-- novelwriter/formats/tokenizer.py | 12 ++++++++++++ novelwriter/formats/tomarkdown.py | 3 +-- novelwriter/formats/toodt.py | 12 ++++-------- novelwriter/formats/toqdoc.py | 3 +-- tests/reference/fmtToDocX_SaveDocument_core.xml | 6 +++--- tests/reference/fmtToDocX_SaveDocument_styles.xml | 2 +- tests/reference/fmtToOdt_SaveFlat_document.fodt | 8 ++++---- tests/test_formats/test_fmt_toodt.py | 5 ----- 12 files changed, 37 insertions(+), 43 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index fb2ff9d3..9e010c55 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -252,6 +252,10 @@ class Config: def hasError(self) -> bool: return self._hasError + @property + def locale(self) -> QLocale: + return self._dLocale + @property def recentProjects(self) -> RecentProjects: return self._recentProjects @@ -469,10 +473,6 @@ class Config: """Return a localised datetime format.""" return self._dLocale.toString(value, self._dShortDateTime) - def localInt(self, value: int) -> str: - """Return a localised integer.""" - return self._dLocale.toString(value) - def listLanguages(self, lngSet: int) -> list[tuple[str, str]]: """List localisation files in the i18n folder. The default GUI language is British English (en_GB). diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index ba3e469d..14b4ed16 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -224,7 +224,9 @@ class NWBuildDocument: # Get Settings textFont = QFont(CONFIG.textFont) textFont.fromString(self._build.getStr("format.textFont")) + bldObj.setFont(textFont) + bldObj.setLanguage(self._project.data.language) bldObj.setPartitionFormat( self._build.getStr("headings.fmtPart"), @@ -322,7 +324,6 @@ class NWBuildDocument: bldObj.setReplaceUnicode(self._build.getBool("format.stripUnicode")) if isinstance(bldObj, (ToOdt, ToDocX)): - bldObj.setLanguage(self._project.data.language) bldObj.setHeaderFormat( self._build.getStr("doc.pageHeader"), self._build.getInt("doc.pageCountOffset"), diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 55cdca21..6461cfe9 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -36,7 +36,7 @@ from zipfile import ZIP_DEFLATED, ZipFile from PyQt5.QtCore import QMarginsF, QSizeF from PyQt5.QtGui import QColor -from novelwriter import CONFIG, __version__ +from novelwriter import __version__ from novelwriter.common import firstFloat, xmlSubElem from novelwriter.constants import nwHeadFmt, nwStyles from novelwriter.core.project import NWProject @@ -182,7 +182,6 @@ class ToDocX(Tokenizer): # Internal self._fontFamily = "Liberation Serif" self._fontSize = 12.0 - self._dLanguage = "en_GB" self._pageSize = QSizeF(210.0, 297.0) self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0) @@ -200,12 +199,6 @@ class ToDocX(Tokenizer): # Setters ## - def setLanguage(self, language: str | None) -> None: - """Set language for the document.""" - if language: - self._dLanguage = language.replace("_", "-") - return - def setPageLayout( self, width: float, height: float, top: float, bottom: float, left: float, right: float ) -> None: @@ -740,7 +733,7 @@ class ToDocX(Tokenizer): xmlSubElem(xRoot, _mkTag("dcterms", "modified"), timeStamp, attrib=tsAttr) xmlSubElem(xRoot, _mkTag("dc", "creator"), self._project.data.author) xmlSubElem(xRoot, _mkTag("dc", "title"), self._project.data.name) - xmlSubElem(xRoot, _mkTag("dc", "language"), self._dLanguage) + xmlSubElem(xRoot, _mkTag("dc", "language"), self._dLocale.name()) xmlSubElem(xRoot, _mkTag("cp", "revision"), str(self._project.data.saveCount)) xmlSubElem(xRoot, _mkTag("cp", "lastModifiedBy"), self._project.data.author) @@ -777,7 +770,7 @@ class ToDocX(Tokenizer): }) xmlSubElem(xRPr, _wTag("sz"), attrib={W_VAL: size}) xmlSubElem(xRPr, _wTag("szCs"), attrib={W_VAL: size}) - xmlSubElem(xRPr, _wTag("lang"), attrib={W_VAL: self._dLanguage}) + xmlSubElem(xRPr, _wTag("lang"), attrib={W_VAL: self._dLocale.name()}) xmlSubElem(xPPr, _wTag("spacing"), attrib={_wTag("line"): line}) # Paragraph Styles @@ -936,7 +929,7 @@ class ToDocX(Tokenizer): if self._usedFields and self._counts: for xField, field in self._usedFields: if (value := self._counts.get(field)) is not None: - xField.text = CONFIG.localInt(value) + xField.text = self._formatInt(value) # Write Paragraphs for par in pars: diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py index ca89e3fe..15ce897a 100644 --- a/novelwriter/formats/tohtml.py +++ b/novelwriter/formats/tohtml.py @@ -29,7 +29,6 @@ import logging from pathlib import Path from time import time -from novelwriter import CONFIG from novelwriter.common import formatTimeStamp from novelwriter.constants import nwHtmlUnicode from novelwriter.core.project import NWProject @@ -259,7 +258,7 @@ class ToHtml(Tokenizer): for doc, field in self._usedFields: if doc >= 0 and doc < pages and (value := self._counts.get(field)) is not None: self._pages[doc] = self._pages[doc].replace( - f"{{{{{field}}}}}", CONFIG.localInt(value) + f"{{{{{field}}}}}", self._formatInt(value) ) # Add footnotes diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 64017e7e..759185c7 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -31,6 +31,7 @@ from abc import ABC, abstractmethod from pathlib import Path from typing import NamedTuple +from PyQt5.QtCore import QLocale from PyQt5.QtGui import QColor, QFont from novelwriter import CONFIG @@ -104,6 +105,7 @@ class Tokenizer(ABC): self._outline: dict[str, str] = {} # User Settings + self._dLocale = CONFIG.locale # The document locale self._textFont = QFont("Serif", 11) # Output text font self._lineHeight = 1.15 # Line height in units of em self._colorHeads = True # Colourise headings @@ -226,6 +228,12 @@ class Tokenizer(ABC): # Setters ## + def setLanguage(self, language: str | None) -> None: + """Set language for the document.""" + if language: + self._dLocale = QLocale(language) + return + def setTheme(self, theme: TextDocumentTheme) -> None: """Set the document colour theme.""" self._theme = theme @@ -1033,6 +1041,10 @@ class Tokenizer(ABC): # Internal Functions ## + def _formatInt(self, value: int) -> str: + """Return a localised integer.""" + return self._dLocale.toString(value) + def _formatComment(self, style: ComStyle, key: str, text: str) -> tuple[str, T_Formats]: """Apply formatting to comments and notes.""" tTxt, tFmt = self._extractFormats(text) diff --git a/novelwriter/formats/tomarkdown.py b/novelwriter/formats/tomarkdown.py index d7981249..f2f1284d 100644 --- a/novelwriter/formats/tomarkdown.py +++ b/novelwriter/formats/tomarkdown.py @@ -27,7 +27,6 @@ import logging from pathlib import Path -from novelwriter import CONFIG from novelwriter.constants import nwUnicode from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt @@ -159,7 +158,7 @@ class ToMarkdown(Tokenizer): for doc, field in self._usedFields: if doc >= 0 and doc < pages and (value := self._counts.get(field)) is not None: self._pages[doc] = self._pages[doc].replace( - f"{{{{{field}}}}}", CONFIG.localInt(value) + f"{{{{{field}}}}}", self._formatInt(value) ) # Add footnotes diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index 0845b14a..b3286170 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -223,14 +223,6 @@ class ToOdt(Tokenizer): # Setters ## - def setLanguage(self, language: str | None) -> None: - """Set language for the document.""" - if language: - lang, _, country = language.partition("_") - self._dLanguage = lang or self._dLanguage - self._dCountry = country or self._dCountry - return - def setPageLayout( self, width: float, height: float, top: float, bottom: float, left: float, right: float ) -> None: @@ -264,6 +256,10 @@ class ToOdt(Tokenizer): fontWeight = str(intWeight) fontBold = str(min(intWeight + 300, 900)) + lang, _, country = self._dLocale.name().partition("_") + self._dLanguage = lang or self._dLanguage + self._dCountry = country or self._dCountry + self._fontFamily = self._textFont.family() self._fontSize = self._textFont.pointSize() self._fontWeight = FONT_WEIGHT_MAP.get(fontWeight, fontWeight) diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 0bb32d4f..de0a344c 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -34,7 +34,6 @@ from PyQt5.QtGui import ( ) from PyQt5.QtPrintSupport import QPrinter -from novelwriter import CONFIG from novelwriter.constants import nwStyles, nwUnicode from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt @@ -259,7 +258,7 @@ class ToQTextDocument(Tokenizer): if (value := self._counts.get(field)) is not None: cursor.setPosition(pos, QtMoveAnchor) cursor.setPosition(pos + 1, QtKeepAnchor) - cursor.insertText(CONFIG.localInt(value)) + cursor.insertText(self._formatInt(value)) # Add footnotes if self._usedNotes: diff --git a/tests/reference/fmtToDocX_SaveDocument_core.xml b/tests/reference/fmtToDocX_SaveDocument_core.xml index 1e503ab2..2d07ced8 100644 --- a/tests/reference/fmtToDocX_SaveDocument_core.xml +++ b/tests/reference/fmtToDocX_SaveDocument_core.xml @@ -1,10 +1,10 @@ - 2024-10-28T18:23:43 - 2024-10-28T18:23:43 + 2024-10-28T20:12:57 + 2024-10-28T20:12:57 lipsum.com Lorem Ipsum - en-GB + en_GB 50 lipsum.com diff --git a/tests/reference/fmtToDocX_SaveDocument_styles.xml b/tests/reference/fmtToDocX_SaveDocument_styles.xml index e2a654e1..f730b33a 100644 --- a/tests/reference/fmtToDocX_SaveDocument_styles.xml +++ b/tests/reference/fmtToDocX_SaveDocument_styles.xml @@ -6,7 +6,7 @@ - + diff --git a/tests/reference/fmtToOdt_SaveFlat_document.fodt b/tests/reference/fmtToOdt_SaveFlat_document.fodt index 3beacd02..508c8cd9 100644 --- a/tests/reference/fmtToOdt_SaveFlat_document.fodt +++ b/tests/reference/fmtToOdt_SaveFlat_document.fodt @@ -1,13 +1,13 @@ - 2024-10-22T14:19:48 - novelWriter/2.6a1 + 2024-10-28T20:17:34 + novelWriter/2.6a3 Jane Smith 1234 P42DT12H34M56S Test Project - 2024-10-22T14:19:48 + 2024-10-28T20:17:34 Jane Smith @@ -16,7 +16,7 @@ - + diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py index a6ebf6b7..536c977e 100644 --- a/tests/test_formats/test_fmt_toodt.py +++ b/tests/test_formats/test_fmt_toodt.py @@ -780,11 +780,6 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths): odt = ToOdt(project, isFlat=True) odt._isNovel = True - odt._dLanguage = "" - odt.setLanguage(None) # type: ignore - assert odt._dLanguage == "" - odt.setLanguage("nb_NO") - assert odt._dLanguage == "nb" odt.setHeaderFormat(nwHeadFmt.DOC_AUTO, 1) assert odt._headerFormat == nwHeadFmt.DOC_AUTO odt.setFirstLineIndent(True, 1.4, False) From 2a3d3e46c230de736705fa0c575392bfd0ed88cc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:27:13 +0100 Subject: [PATCH 09/11] Pin tests to set locale to en_GB --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 5323f93f..4ec3eeb3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,6 +28,7 @@ from pathlib import Path import pytest +from PyQt5.QtCore import QLocale from PyQt5.QtWidgets import QMessageBox sys.path.insert(1, str(Path(__file__).parent.parent.absolute())) @@ -54,6 +55,7 @@ def resetConfigVars(): CONFIG.setGuiFont(None) CONFIG.setTextFont(None) CONFIG._homePath = _TMP_ROOT + CONFIG._dLocale = QLocale("en_GB") CONFIG.guiLocale = "en_GB" return From 3749c9f21d4c6dd7a804eae5064734c2ea9b2ef2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:15:58 +0100 Subject: [PATCH 10/11] Fix variable formatting for DocX and ODT --- novelwriter/common.py | 33 ++++++++- novelwriter/constants.py | 4 +- novelwriter/enum.py | 6 +- novelwriter/formats/todocx.py | 74 +++++++++---------- novelwriter/formats/toodt.py | 46 ++++++++---- .../fmtToDocX_SaveDocument_document.xml | 2 + .../reference/fmtToOdt_SaveFlat_document.fodt | 9 ++- tests/reference/fmtToOdt_SaveFull_styles.xml | 5 +- ...uildDocBuild_OpenDocument_Lorem_Ipsum.fodt | 13 ++-- tests/test_base/test_base_common.py | 30 +++++++- tests/test_core/test_core_docbuild.py | 54 ++++++++------ 11 files changed, 181 insertions(+), 95 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 397f6dc5..bf150794 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -492,6 +492,10 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: return "".join(buffer) +## +# XML Helpers +## + def xmlIndent(tree: ET.Element | ET.ElementTree) -> None: """A modified version of the XML indent function in the standard library. It behaves more closely to how the one from lxml does. @@ -535,21 +539,42 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None: return +def xmlElement( + tag: str, + text: str | int | float | bool | None = None, + *, + attrib: dict | None = None, + tail: str | None = None, +) -> ET.Element: + """A custom implementation of Element with more arguments.""" + xSub = ET.Element(tag, attrib=attrib or {}) + if text is not None: + if isinstance(text, bool): + xSub.text = str(text).lower() + else: + xSub.text = str(text) + if tail is not None: + xSub.tail = tail + return xSub + + def xmlSubElem( parent: ET.Element, tag: str, text: str | int | float | bool | None = None, - attrib: dict | None = None + *, + attrib: dict | None = None, + tail: str | None = None, ) -> ET.Element: - """A custom implementation of SubElement that takes text as an - argument. - """ + """A custom implementation of SubElement with more arguments.""" xSub = ET.SubElement(parent, tag, attrib=attrib or {}) if text is not None: if isinstance(text, bool): xSub.text = str(text).lower() else: xSub.text = str(text) + if tail is not None: + xSub.tail = tail return xSub diff --git a/novelwriter/constants.py b/novelwriter/constants.py index af232328..954f21ed 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -334,12 +334,12 @@ class nwLabels: nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"), nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"), nwBuildFmt.DOCX: QT_TRANSLATE_NOOP("Constant", "Microsoft Word Document (.docx)"), - nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "novelWriter HTML (.html)"), + nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "HTML 5 (.html)"), nwBuildFmt.NWD: QT_TRANSLATE_NOOP("Constant", "novelWriter Markup (.txt)"), nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"), nwBuildFmt.EXT_MD: QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)"), nwBuildFmt.PDF: QT_TRANSLATE_NOOP("Constant", "Portable Document Format (.pdf)"), - nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter HTML (.json)"), + nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + HTML 5 (.json)"), nwBuildFmt.J_NWD: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markup (.json)"), } BUILD_EXT = { diff --git a/novelwriter/enum.py b/novelwriter/enum.py index f59a7047..b6135344 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -184,11 +184,11 @@ class nwBuildFmt(Enum): ODT = 0 FODT = 1 DOCX = 2 - HTML = 3 - NWD = 4 + PDF = 3 + HTML = 4 STD_MD = 5 EXT_MD = 6 - PDF = 7 + NWD = 7 J_HTML = 8 J_NWD = 9 diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 6461cfe9..82ed2e36 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -37,7 +37,7 @@ from PyQt5.QtCore import QMarginsF, QSizeF from PyQt5.QtGui import QColor from novelwriter import __version__ -from novelwriter.common import firstFloat, xmlSubElem +from novelwriter.common import firstFloat, xmlElement, xmlSubElem from novelwriter.constants import nwHeadFmt, nwStyles from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt @@ -57,13 +57,13 @@ RELS_BASE = f"{OOXML_SCM}/officeDocument/2006/relationships" # Main XML NameSpaces XML_NS = { - "r": RELS_BASE, - "w": f"{OOXML_SCM}/wordprocessingml/2006/main", "cp": f"{OOXML_SCM}/package/2006/metadata/core-properties", "dc": "http://purl.org/dc/elements/1.1/", - "xsi": "http://www.w3.org/2001/XMLSchema-instance", - "xml": "http://www.w3.org/XML/1998/namespace", "dcterms": "http://purl.org/dc/terms/", + "r": RELS_BASE, + "w": f"{OOXML_SCM}/wordprocessingml/2006/main", + "xml": "http://www.w3.org/XML/1998/namespace", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", } for ns, uri in XML_NS.items(): ET.register_namespace(ns, uri) @@ -328,10 +328,10 @@ class ToDocX(Tokenizer): cDocs.append(("/word/_rels/document.xml.rels", RELS_TYPE)) # Relationships XML - rRels = ET.Element("Relationships", attrib={ + rRels = xmlElement("Relationships", attrib={ "xmlns": f"{OOXML_SCM}/package/2006/relationships" }) - wRels = ET.Element("Relationships", attrib={ + wRels = xmlElement("Relationships", attrib={ "xmlns": f"{OOXML_SCM}/package/2006/relationships" }) for name, rel in self._rels.items(): @@ -347,7 +347,7 @@ class ToDocX(Tokenizer): xmlSubElem(rRels if isRoot else wRels, "Relationship", attrib=attrib) # Content Types XML - dTypes = ET.Element("Types", attrib={ + dTypes = xmlElement("Types", attrib={ "xmlns": f"{OOXML_SCM}/package/2006/content-types" }) for name, content in cExts: @@ -355,10 +355,8 @@ class ToDocX(Tokenizer): for name, content in cDocs: xmlSubElem(dTypes, "Override", attrib={"PartName": name, "ContentType": content}) - def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None: - with zipObj.open(name, mode="w") as fObj: - xml = ET.ElementTree(xObj) - xml.write(fObj, encoding="utf-8", xml_declaration=True) + def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None: + zipObj.writestr(name, ET.tostring(root, encoding="utf-8", xml_declaration=True)) with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip: xmlToZip("_rels/.rels", rRels, outZip) @@ -435,7 +433,7 @@ class ToDocX(Tokenizer): elif fFmt == TextFmt.FNOTE: xNode = self._generateFootnote(fData) elif fFmt == TextFmt.FIELD: - xNode = self._generateField(fData) + xNode = self._generateField(fData, xFmt) elif fFmt == TextFmt.STRIP: pass @@ -450,9 +448,9 @@ class ToDocX(Tokenizer): return - def _textRunToXml(self, text: str, fmt: int, fClass: str, fLink: str) -> ET.Element: + def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element: """Encode the text run into XML.""" - xR = ET.Element(_wTag("r")) + xR = xmlElement(_wTag("r")) rPr = xmlSubElem(xR, _wTag("rPr")) if fmt & X_BLD: xmlSubElem(rPr, _wTag("b")) @@ -473,18 +471,20 @@ class ToDocX(Tokenizer): if fmt & X_COL and (color := self._classes.get(fClass)): xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(color)}) - for segment in RX_TEXT.split(text): - if segment == "\n": - xmlSubElem(xR, _wTag("br")) - elif segment == "\t": - xmlSubElem(xR, _wTag("tab")) - elif segment: - _wText(xR, segment) + if isinstance(text, str): + for segment in RX_TEXT.split(text): + if segment == "\n": + xmlSubElem(xR, _wTag("br")) + elif segment == "\t": + xmlSubElem(xR, _wTag("tab")) + elif segment: + _wText(xR, segment) if fmt & X_HRF and fLink: xmlSubElem(rPr, _wTag("rStyle"), attrib={W_VAL: "InternetLink"}) - rId = self._appendExternalRel(fLink) - xH = ET.Element(_wTag("hyperlink"), attrib={_mkTag("r", "id"): rId}) + xH = xmlElement(_wTag("hyperlink"), attrib={ + _mkTag("r", "id"): self._appendExternalRel(fLink), + }) xH.append(xR) return xH @@ -498,7 +498,7 @@ class ToDocX(Tokenizer): """Generate a footnote XML object.""" if key in self._footnotes: idx = len(self._usedNotes) + 1 - xR = ET.Element(_wTag("r")) + xR = xmlElement(_wTag("r")) rPr = xmlSubElem(xR, _wTag("rPr")) xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"}) xmlSubElem(xR, _wTag("footnoteReference"), attrib={_wTag("id"): str(idx)}) @@ -506,11 +506,11 @@ class ToDocX(Tokenizer): return xR return None - def _generateField(self, key: str) -> ET.Element | None: + def _generateField(self, key: str, fmt: int) -> ET.Element | None: """Generate a data field XML object.""" if key and (field := key.partition(":")[2]): - xR = ET.Element(_wTag("r")) - xT = xmlSubElem(xR, _wTag("t"), "0") + xR = self._textRunToXml(None, fmt, "", "") + xT = _wText(xR, "0") self._usedFields.append((xT, field)) return xR return None @@ -687,7 +687,7 @@ class ToDocX(Tokenizer): def _appXml(self) -> str: """Populate app.xml.""" rId = self._nextRelId() - xRoot = ET.Element("Properties", attrib={ + xRoot = xmlElement("Properties", attrib={ "xmlns": f"{OOXML_SCM}/officeDocument/2006/extended-properties" }) self._rels["app.xml"] = DocXXmlRel( @@ -716,7 +716,7 @@ class ToDocX(Tokenizer): def _coreXml(self) -> str: """Populate app.xml.""" rId = self._nextRelId() - xRoot = ET.Element(_mkTag("cp", "coreProperties")) + xRoot = xmlElement(_mkTag("cp", "coreProperties")) self._rels["core.xml"] = DocXXmlRel( rId=rId, relType=f"{OOXML_SCM}/package/2006/relationships/metadata/core-properties", @@ -742,7 +742,7 @@ class ToDocX(Tokenizer): def _stylesXml(self) -> str: """Populate styles.xml.""" rId = self._nextRelId() - xRoot = ET.Element(_wTag("styles")) + xRoot = xmlElement(_wTag("styles")) self._rels["styles.xml"] = DocXXmlRel( rId=rId, relType=f"{RELS_BASE}/styles", @@ -830,7 +830,7 @@ class ToDocX(Tokenizer): def _defaultHeaderXml(self) -> str: """Populate header1.xml.""" rId = self._nextRelId() - xRoot = ET.Element(_wTag("hdr")) + xRoot = xmlElement(_wTag("hdr")) self._rels["header1.xml"] = DocXXmlRel( rId=rId, relType=f"{RELS_BASE}/header", @@ -877,7 +877,7 @@ class ToDocX(Tokenizer): def _firstHeaderXml(self) -> str: """Populate header2.xml.""" rId = self._nextRelId() - xRoot = ET.Element(_wTag("hdr")) + xRoot = xmlElement(_wTag("hdr")) self._rels["header2.xml"] = DocXXmlRel( rId=rId, relType=f"{RELS_BASE}/header", @@ -902,7 +902,7 @@ class ToDocX(Tokenizer): def _documentXml(self, hFirst: str | None, hDefault: str | None) -> str: """Populate document.xml.""" rId = self._nextRelId() - xRoot = ET.Element(_wTag("document")) + xRoot = xmlElement(_wTag("document")) xRoot.set("xmlns:w14", "http://schemas.microsoft.com/office/word/2010/wordml") xBody = xmlSubElem(xRoot, _wTag("body")) self._rels["document.xml"] = DocXXmlRel( @@ -978,7 +978,7 @@ class ToDocX(Tokenizer): def _footnotesXml(self) -> str: """Populate footnotes.xml.""" rId = self._nextRelId() - xRoot = ET.Element(_wTag("footnotes")) + xRoot = xmlElement(_wTag("footnotes")) self._rels["footnotes.xml"] = DocXXmlRel( rId=rId, relType=f"{RELS_BASE}/footnotes", @@ -1001,7 +1001,7 @@ class ToDocX(Tokenizer): def _fontTableXml(self) -> str: """Populate fontTable.xml.""" rId = self._nextRelId() - xRoot = ET.Element(_wTag("fonts")) + xRoot = xmlElement(_wTag("fonts")) self._rels["fontTable.xml"] = DocXXmlRel( rId=rId, relType=f"{RELS_BASE}/fontTable", @@ -1024,7 +1024,7 @@ class ToDocX(Tokenizer): def _settingsXml(self) -> str: """Populate settings.xml.""" rId = self._nextRelId() - xRoot = ET.Element(_wTag("settings")) + xRoot = xmlElement(_wTag("settings")) self._rels["settings.xml"] = DocXXmlRel( rId=rId, relType=f"{RELS_BASE}/settings", diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index b3286170..f72aa6a4 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -38,7 +38,7 @@ from zipfile import ZIP_DEFLATED, ZipFile from PyQt5.QtGui import QColor, QFont from novelwriter import __version__ -from novelwriter.common import xmlIndent, xmlSubElem +from novelwriter.common import xmlElement, xmlIndent, xmlSubElem from novelwriter.constants import nwHeadFmt, nwStyles from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape @@ -54,6 +54,7 @@ XML_NS = { "loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0", "manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0", "meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0", + "number": "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0", "office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0", "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0", "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0", @@ -115,6 +116,7 @@ S_FIND = "First_20_line_20_indent" S_TEXT = "Text_20_body" S_META = "Text_20_Meta" S_HNF = "Header_20_and_20_Footer" +S_NUM = "N0" # Font Data FONT_WEIGHT_NUM = ["100", "200", "300", "400", "500", "600", "700", "800", "900"] @@ -513,13 +515,14 @@ class ToOdt(Tokenizer): oVers = _mkTag("office", "version") xSett = ET.Element(oRoot, attrib={oVers: X_VERS}) - def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None: - with zipObj.open(name, mode="w") as fObj: - xml = ET.ElementTree(xObj) - xml.write(fObj, encoding="utf-8", xml_declaration=True) + def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None: + zipObj.writestr( + name, ET.tostring(root, encoding="utf-8", xml_declaration=True), + compress_type=ZIP_DEFLATED, compresslevel=3, + ) - with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip: - outZip.writestr("mimetype", X_MIME) + with ZipFile(path, mode="w") as outZip: + outZip.writestr("mimetype", X_MIME, compress_type=None, compresslevel=None) xmlToZip("META-INF/manifest.xml", xMani, outZip) xmlToZip("settings.xml", xSett, outZip) xmlToZip("content.xml", self._dCont, outZip) @@ -627,7 +630,7 @@ class ToOdt(Tokenizer): elif fFmt == TextFmt.FNOTE: xNode = self._generateFootnote(fData) elif fFmt == TextFmt.FIELD: - xNode = self._generateField(fData) + xNode = self._generateField(fData, xFmt) elif fFmt == TextFmt.STRIP: pass @@ -713,24 +716,31 @@ class ToOdt(Tokenizer): if content := self._footnotes.get(key): self._nNote += 1 nStyle = ODTParagraphStyle("New") - xNote = ET.Element(_mkTag("text", "note"), attrib={ + xNote = xmlElement(_mkTag("text", "note"), attrib={ _mkTag("text", "id"): f"ftn{self._nNote}", _mkTag("text", "note-class"): "footnote", }) xmlSubElem(xNote, _mkTag("text", "note-citation"), self._nNote) - xBody = ET.SubElement(xNote, _mkTag("text", "note-body")) + xBody = xmlSubElem(xNote, _mkTag("text", "note-body")) self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1]) return xNote return None - def _generateField(self, key: str) -> ET.Element | None: + def _generateField(self, key: str, fmt: int) -> ET.Element | None: """Generate a data field XML object.""" if key and (field := key.partition(":")[2]): - xField = ET.Element(_mkTag("text", "user-field-get"), attrib={ + xField = xmlElement(_mkTag("text", "user-field-get"), "0", tail="", attrib={ + _mkTag("style", "data-style-name"): S_NUM, _mkTag("text", "name"): f"Manuscript{field[:1].upper()}{field[1:]}", }) - xField.text = "0" - return xField + if fmt == 0x00: + return xField + else: + xSpan = xmlElement(TAG_SPAN, "", tail="", attrib={ + _mkTag("text", "style-name"): self._textStyle(fmt), + }) + xSpan.append(xField) + return xSpan return None def _emToCm(self, value: float) -> str: @@ -836,6 +846,14 @@ class ToOdt(Tokenizer): _mkTag("style", "class"): "extra", }) + # Numbers Style + xStyl = ET.SubElement(self._xStyl, _mkTag("number", "number-style"), attrib={ + _mkTag("style", "name"): S_NUM, + }) + ET.SubElement(xStyl, _mkTag("number", "number"), attrib={ + _mkTag("number", "min-integer-digits"): "1", + }) + return def _useableStyles(self) -> None: diff --git a/tests/reference/fmtToDocX_SaveDocument_document.xml b/tests/reference/fmtToDocX_SaveDocument_document.xml index 4d840bc5..132468c6 100644 --- a/tests/reference/fmtToDocX_SaveDocument_document.xml +++ b/tests/reference/fmtToDocX_SaveDocument_document.xml @@ -33,6 +33,7 @@ Word Count: + 4,035 @@ -46,6 +47,7 @@ Character Count: + 27,064 diff --git a/tests/reference/fmtToOdt_SaveFlat_document.fodt b/tests/reference/fmtToOdt_SaveFlat_document.fodt index 508c8cd9..1e82c1c6 100644 --- a/tests/reference/fmtToOdt_SaveFlat_document.fodt +++ b/tests/reference/fmtToOdt_SaveFlat_document.fodt @@ -1,13 +1,13 @@ - + - 2024-10-28T20:17:34 + 2024-10-29T09:33:22 novelWriter/2.6a3 Jane Smith 1234 P42DT12H34M56S Test Project - 2024-10-28T20:17:34 + 2024-10-29T09:33:22 Jane Smith @@ -26,6 +26,9 @@ + + + diff --git a/tests/reference/fmtToOdt_SaveFull_styles.xml b/tests/reference/fmtToOdt_SaveFull_styles.xml index aa1f8a94..9b34773a 100644 --- a/tests/reference/fmtToOdt_SaveFull_styles.xml +++ b/tests/reference/fmtToOdt_SaveFull_styles.xml @@ -1,5 +1,5 @@ - + @@ -16,6 +16,9 @@ + + + diff --git a/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt b/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt index 6299ba17..9f61c7a4 100644 --- a/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt +++ b/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt @@ -1,13 +1,13 @@ - + - 2024-10-28T18:27:00 + 2024-10-29T09:41:30 novelWriter/2.6a3 lipsum.com 50 P0DT0H40M48S Lorem Ipsum - 2024-10-28T18:27:00 + 2024-10-29T09:41:30 lipsum.com @@ -26,6 +26,9 @@ + + + @@ -164,8 +167,8 @@ Lorem Ipsum By lipsum.com - Word Count: 0 - Character Count: 0 + Word Count: 0 + Character Count: 0 “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” Comment: Exctracted from the lipsum.com website. diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 9c1f8dff..8eae1ba0 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -37,8 +37,8 @@ from novelwriter.common import ( formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe, minmax, numberToRoman, openExternalPath, - readTextFile, simplified, transferCase, uniqueCompact, xmlIndent, - xmlSubElem, yesNo + readTextFile, simplified, transferCase, uniqueCompact, xmlElement, + xmlIndent, xmlSubElem, yesNo ) from tests.mocked import causeOSError @@ -634,6 +634,29 @@ def testBaseCommon_xmlIndent(): assert data == "foobar" +@pytest.mark.base +def testBaseCommon_xmlElement(): + """Test the xmlElement function.""" + assert ET.tostring( + xmlElement("node", None, attrib={"a": "b"}) + ) == b'' + assert ET.tostring( + xmlElement("node", "text", attrib={"a": "b"}) + ) == b'text' + assert ET.tostring( + xmlElement("node", "text", tail="foo", attrib={"a": "b"}) + ) == b'textfoo' + assert ET.tostring( + xmlElement("node", 42, attrib={"a": "b"}) + ) == b'42' + assert ET.tostring( + xmlElement("node", 3.14, attrib={"a": "b"}) + ) == b'3.14' + assert ET.tostring( + xmlElement("node", True, attrib={"a": "b"}) + ) == b'true' + + @pytest.mark.base def testBaseCommon_xmlSubElem(): """Test the xmlSubElem function.""" @@ -643,6 +666,9 @@ def testBaseCommon_xmlSubElem(): assert ET.tostring( xmlSubElem(ET.Element("r"), "node", "text", attrib={"a": "b"}) ) == b'text' + assert ET.tostring( + xmlSubElem(ET.Element("r"), "node", "text", tail="foo", attrib={"a": "b"}) + ) == b'textfoo' assert ET.tostring( xmlSubElem(ET.Element("r"), "node", 42, attrib={"a": "b"}) ) == b'42' diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py index 713aab50..04ec4e75 100644 --- a/tests/test_core/test_core_docbuild.py +++ b/tests/test_core/test_core_docbuild.py @@ -321,7 +321,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths @pytest.mark.core -def testCoreDocBuild_DocX(monkeypatch, mockGUI, prjLipsum, fncPath): +def testCoreDocBuild_DocX(mockGUI, prjLipsum, fncPath): """Test building a Word manuscript.""" project = NWProject() project.openProject(prjLipsum) @@ -352,22 +352,41 @@ def testCoreDocBuild_DocX(monkeypatch, mockGUI, prjLipsum, fncPath): assert docFile.is_file() assert zipfile.is_zipfile(docFile) - # Check Error Handling - # ==================== - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) +@pytest.mark.core +def testCoreDocBuild_PDF(mockGUI, prjLipsum, fncPath): + """Test building a PDF manuscript.""" + project = NWProject() + project.openProject(prjLipsum) - docFile = fncPath / "Lorem Ipsum Err.fodt" - for _ in docBuild.iterBuildDocument(docFile, nwBuildFmt.FODT): - pass + build = BuildSettings() + build.unpack(BUILD_CONF) - assert docBuild.error == "OSError: Mock OSError" - assert not docFile.is_file() + docBuild = NWBuildDocument(project, build) + docBuild.queueAll() + + assert len(docBuild) == 21 + + # Check Build + # =========== + + docFile = fncPath / "Lorem Ipsum.pdf" + + count = 0 + error = [] + for _, success in docBuild.iterBuildDocument(docFile, nwBuildFmt.PDF): + count += 1 if success else 0 + if docBuild.error: + error.append(docBuild.error) + + assert count == 19 + assert error == [] + + assert docFile.is_file() @pytest.mark.core -def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): +def testCoreDocBuild_NWD(mockGUI, prjLipsum, fncPath, tstPaths): """Test building a NWD manuscript.""" project = NWProject() project.openProject(prjLipsum) @@ -420,19 +439,6 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): copyfile(docFile, tstFile) assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6]) - # Check Error Handling - # ==================== - - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - - docFile = fncPath / "Lorem Ipsum Err.md" - for _ in docBuild.iterBuildDocument(docFile, nwBuildFmt.NWD): - pass - - assert docBuild.error == "OSError: Mock OSError" - assert not docFile.is_file() - @pytest.mark.core def testCoreDocBuild_Custom(mockGUI, fncPath: Path): From 3d935e29eb80ca18981efa7d1820024def4d42a6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 30 Oct 2024 00:08:31 +0100 Subject: [PATCH 11/11] Update test coverage --- novelwriter/formats/todocx.py | 1 - novelwriter/gui/doceditor.py | 8 +- .../reference/fmtToOdt_SaveFlat_document.fodt | 38 +++++++-- tests/reference/fmtToOdt_SaveFull_content.xml | 42 ++++++++- .../guiEditor_Main_Final_000000000000f.nwd | 8 +- .../guiEditor_Main_Final_nwProject.nwx | 6 +- tests/test_core/test_core_index.py | 1 + tests/test_formats/test_fmt_todocx.py | 66 +++++++++----- tests/test_formats/test_fmt_tokenizer.py | 5 +- tests/test_formats/test_fmt_toodt.py | 85 +++++++++++++------ tests/test_gui/test_gui_doceditor.py | 24 ------ tests/test_gui/test_gui_guimain.py | 9 +- tests/tools.py | 10 +++ 13 files changed, 206 insertions(+), 97 deletions(-) diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 82ed2e36..fe634b5e 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -903,7 +903,6 @@ class ToDocX(Tokenizer): """Populate document.xml.""" rId = self._nextRelId() xRoot = xmlElement(_wTag("document")) - xRoot.set("xmlns:w14", "http://schemas.microsoft.com/office/word/2010/wordml") xBody = xmlSubElem(xRoot, _wTag("body")) self._rels["document.xml"] = DocXXmlRel( rId=rId, diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 2d98567d..04e9cf94 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2065,12 +2065,8 @@ class GuiDocEditor(QPlainTextEdit): feature for French, Spanish, etc, so it doesn't insert a space before colons in meta data lines. See issue #1090. """ - if char == ":" and len(text) > 1: - if text[0] == "@": - return False - if text[0] == "%": - if text[1:].lstrip()[:9].lower() == "synopsis:": - return False + if char == ":" and len(text) > 1 and text[0] == "@": + return False return True def _autoSelect(self) -> QTextCursor: diff --git a/tests/reference/fmtToOdt_SaveFlat_document.fodt b/tests/reference/fmtToOdt_SaveFlat_document.fodt index 1e82c1c6..0b500e35 100644 --- a/tests/reference/fmtToOdt_SaveFlat_document.fodt +++ b/tests/reference/fmtToOdt_SaveFlat_document.fodt @@ -1,13 +1,13 @@ - + - 2024-10-29T09:33:22 + 2024-10-29T23:24:19 novelWriter/2.6a3 Jane Smith 1234 P42DT12H34M56S Test Project - 2024-10-29T09:33:22 + 2024-10-29T23:24:19 Jane Smith @@ -82,6 +82,12 @@ + + + + + + @@ -95,10 +101,30 @@ - Chapter One - Text + + + + + + + + + + + + + + My Novel + Word Count: 00 paragrphsWeb: http://example.com + Chapter One + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum. Chapter Two - Text + Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis. + 1 + + Lorem ipsum + + diff --git a/tests/reference/fmtToOdt_SaveFull_content.xml b/tests/reference/fmtToOdt_SaveFull_content.xml index 0654b0f3..4c99fd4b 100644 --- a/tests/reference/fmtToOdt_SaveFull_content.xml +++ b/tests/reference/fmtToOdt_SaveFull_content.xml @@ -1,5 +1,5 @@ - + @@ -7,13 +7,47 @@ + + + + + + - Chapter One - Text + + + + + + + + + + + + + + My Novel + + Word Count: + + 0 + + + 0 paragrphsWeb: http://example.com + + Chapter One + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum. Chapter Two - Text + Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis. + 1 + + Lorem ipsum + + + diff --git a/tests/reference/guiEditor_Main_Final_000000000000f.nwd b/tests/reference/guiEditor_Main_Final_000000000000f.nwd index b97e07c1..fe96f10a 100644 --- a/tests/reference/guiEditor_Main_Final_000000000000f.nwd +++ b/tests/reference/guiEditor_Main_Final_000000000000f.nwd @@ -1,8 +1,8 @@ %%~name: New Scene %%~path: 000000000000d/000000000000f %%~kind: NOVEL/DOCUMENT -%%~hash: 0566024662fb6ed7ed645717addd64dabf058915 -%%~date: 2024-10-27 13:03:13/2024-10-27 13:03:18 +%%~hash: 89b54ddaec2fddfd220e91dd438c84fb3aef9fc2 +%%~date: 2024-10-30 00:07:21/2024-10-30 00:07:26 # Novel ## Chapter @@ -40,7 +40,9 @@ Some “ double quoted text with spaces padded ”. @object: NoSpaceAdded -% synopsis: No space before this colon. +% synopsis : Space before this is OK. + +%Footnote.abc : A simple footnote. Add space before this colon : See? diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 55133ebe..66c1939d 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,6 +1,6 @@ - - + + New Project Jane Doe @@ -46,7 +46,7 @@ New Chapter - + New Scene diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 8effcd2a..f4562d5b 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -1490,5 +1490,6 @@ def testCoreIndex_processComment(): # Padding with term assert processComment("%note.term: Hi") == (nwComment.NOTE, "term", "Hi", 6, 11) assert processComment("% note.term: Hi") == (nwComment.NOTE, "term", "Hi", 7, 12) + assert processComment("% note.term : Hi") == (nwComment.NOTE, "term", "Hi", 7, 13) assert processComment("% note. term : Hi") == (nwComment.PLAIN, "", "note. term : Hi", 0, 0) assert processComment("% note . term : Hi") == (nwComment.PLAIN, "", "note . term : Hi", 0, 0) diff --git a/tests/test_formats/test_fmt_todocx.py b/tests/test_formats/test_fmt_todocx.py index e4ecc793..2ae740ba 100644 --- a/tests/test_formats/test_fmt_todocx.py +++ b/tests/test_formats/test_fmt_todocx.py @@ -32,28 +32,9 @@ from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.project import NWProject from novelwriter.enum import nwBuildFmt from novelwriter.formats.shared import BlockFmt, BlockTyp -from novelwriter.formats.todocx import ToDocX, _mkTag, _wTag +from novelwriter.formats.todocx import OOXML_SCM, ToDocX, _mkTag, _wTag -from tests.tools import DOCX_IGNORE, cmpFiles - -OOXML_SCM = "http://schemas.openxmlformats.org" -XML_NS = [ - f' xmlns:r="{OOXML_SCM}/officeDocument/2006/relationships"', - f' xmlns:w="{OOXML_SCM}/wordprocessingml/2006/main"', - f' xmlns:cp="{OOXML_SCM}/package/2006/metadata/core-properties"', - ' xmlns:dc="http://purl.org/dc/elements/1.1/"', - ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"', - ' xmlns:xml="http://www.w3.org/XML/1998/namespace"', - ' xmlns:dcterms="http://purl.org/dc/terms/"', -] - - -def xmlToText(xElem): - """Get the text content of an XML element.""" - rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode() - for ns in XML_NS: - rTxt = rTxt.replace(ns, "") - return rTxt +from tests.tools import DOCX_IGNORE, cmpFiles, xmlToText @pytest.mark.core @@ -610,6 +591,49 @@ def testFmtToDocX_Footnotes(mockGUI): ) +@pytest.mark.core +def testFmtToDocX_Fields(mockGUI): + """Test formatting of footnotes.""" + project = NWProject() + doc = ToDocX(project) + doc.initDocument() + + # Field Builder + xNode = doc._generateField("a:b", 0x00) + assert isinstance(xNode, ET.Element) + assert xmlToText(xNode) == "0" + assert doc._usedFields == [(xNode.find(_wTag("t")), "b")] + + assert doc._generateField("a", 0x00) is None + + # Full Processing + doc._text = ( + "Word Count: [field:allWords]\n" + "Character Count: [field:allChars]\n" + "Chicken Count: [field:allChickens]\n" + ) + doc.tokenizeText() + doc.doConvert() + doc.countStats() + doc._documentXml(None, None) + assert xmlToText(doc._files["document.xml"].xml) == ( + '' + 'Word Count: ' + '6' + 'Character Count: ' + '46' + 'Chicken Count: ' + '0' + '' + '' + '' + '' + '' + '' + ) + + @pytest.mark.core def testFmtToDocX_SaveDocument(mockGUI, prjLipsum, fncPath, tstPaths): """Test document output.""" diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 4b3178b8..f4dfdb7b 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -45,7 +45,7 @@ class BareTokenizer(Tokenizer): super().doConvert() # type: ignore (deliberate check) def closeDocument(self): - super().doConvert() # type: ignore (deliberate check) + super().closeDocument() # type: ignore (deliberate check) def saveDocument(self, path) -> None: super().saveDocument(path) # type: ignore (deliberate check) @@ -60,6 +60,9 @@ def testFmtToken_Abstracts(mockGUI, tstPaths): with pytest.raises(NotImplementedError): tokens.doConvert() + with pytest.raises(NotImplementedError): + tokens.closeDocument() + with pytest.raises(NotImplementedError): tokens.saveDocument(tstPaths) diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py index 536c977e..18d128d7 100644 --- a/tests/test_formats/test_fmt_toodt.py +++ b/tests/test_formats/test_fmt_toodt.py @@ -35,26 +35,7 @@ from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt from novelwriter.formats.toodt import ODTParagraphStyle, ODTTextStyle, ToOdt, XMLParagraph, _mkTag -from tests.tools import ODT_IGNORE, cmpFiles - -XML_NS = [ - ' xmlns:dc="http://purl.org/dc/elements/1.1/"', - ' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"', - ' xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"', - ' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"', - ' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"', - ' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"', - ' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"', - ' xmlns:xlink="http://www.w3.org/1999/xlink"', -] - - -def xmlToText(xElem): - """Get the text content of an XML element.""" - rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode() - for ns in XML_NS: - rTxt = rTxt.replace(ns, "") - return rTxt +from tests.tools import ODT_IGNORE, cmpFiles, xmlToText @pytest.mark.core @@ -237,6 +218,46 @@ def testFmtToOdt_TextFormatting(mockGUI): ) +@pytest.mark.core +def testFmtToOdt_Fields(mockGUI): + """Test formatting of footnotes.""" + project = NWProject() + odt = ToOdt(project, True) + odt.initDocument() + + # Field Builder + xNode = odt._generateField("a:allWords", 0x00) + assert isinstance(xNode, ET.Element) + assert xmlToText(xNode) == ( + '' + '0' + ) + # assert odt._usedFields == [(xNode.find(_wTag("t")), "b")] + + assert odt._generateField("a", 0x00) is None + + # Full Processing + odt._text = ( + "Word Count: [field:allWords]\n" + "Character Count: [field:allChars]\n" + "Chicken Count: [field:allChickens]\n" + ) + odt.tokenizeText() + odt.doConvert() + odt.countStats() + assert xmlToText(odt._xBody) == ( + '' + '' + 'Word Count: 0' + 'Character Count: 0' + 'Chicken Count: 0' + '' + ) + + @pytest.mark.core def testFmtToOdt_DialogueFormatting(mockGUI): """Test formatting of dialogue.""" @@ -770,7 +791,7 @@ def testFmtToOdt_ConvertDirect(mockGUI): @pytest.mark.core -def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths): +def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths, ipsumText): """Test the document save functions.""" project = NWProject() project.data.setAuthor("Jane Smith") @@ -796,14 +817,20 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths): assert odt._mDocRight == "1.500cm" odt._text = ( + "#! My Novel\n\n" + "**Word Count: [field:allWords]**\n" + "[field:paragraphCount] paragrphs\n" + "Web: http://example.com\n\n" "## Chapter One\n\n" - "Text\n\n" + f"{ipsumText[0]}\n\n" "## Chapter Two\n\n" - "Text\n\n" + f"{ipsumText[1]}[footnote:abc]\n\n" + "%Footnote.abc: Lorem ipsum\n\n" ) odt.tokenizeText() odt.initDocument() odt.doConvert() + odt.countStats() odt.closeDocument() flatFile = fncPath / "document.fodt" @@ -818,7 +845,7 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths): @pytest.mark.core -def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths): +def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths, ipsumText): """Test the document save functions.""" project = NWProject() project.data.setAuthor("Jane Smith") @@ -833,14 +860,20 @@ def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths): odt.setHeaderFormat(f"{nwHeadFmt.DOC_PROJECT} - {nwHeadFmt.DOC_AUTHOR}", 0) odt._text = ( + "#! My Novel\n\n" + "**Word Count: [field:allWords]**\n" + "[field:paragraphCount] paragrphs\n" + "Web: http://example.com\n\n" "## Chapter One\n\n" - "Text\n\n" + f"{ipsumText[0]}\n\n" "## Chapter Two\n\n" - "Text\n\n" + f"{ipsumText[1]}[footnote:abc]\n\n" + "%Footnote.abc: Lorem ipsum\n\n" ) odt.tokenizeText() odt.initDocument() odt.doConvert() + odt.countStats() odt.closeDocument() fullFile = fncPath / "document.odt" diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 3910e7b1..62c60859 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -2184,27 +2184,3 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): assert docEditor.textCursor().selectedText() == "" # qtbot.stop() - - -@pytest.mark.gui -def testGuiEditor_StaticMethods(): - """Test the document editor's static methods.""" - # Check the method that decides if it is allowed to insert a space - # before a colon using the French, Spanish, etc language feature - assert GuiDocEditor._allowSpaceBeforeColon("", "") is True - assert GuiDocEditor._allowSpaceBeforeColon("", ":") is True - assert GuiDocEditor._allowSpaceBeforeColon("some text", ":") is True - - assert GuiDocEditor._allowSpaceBeforeColon("@:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("@>", ">") is True - - assert GuiDocEditor._allowSpaceBeforeColon("%", ":") is True - assert GuiDocEditor._allowSpaceBeforeColon("%:", ":") is True - assert GuiDocEditor._allowSpaceBeforeColon("%synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("%synopsis :", ":") is True - assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis :", ":") is True diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index f09d885f..eb87ac31 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -474,7 +474,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): # Special Formatting # ================== - # Insert spaces before colon, but ignore tags and synopsis + # Insert spaces before colon, but ignore tags docEditor._typPadBefore = ":" for c in "@object: NoSpaceAdded": @@ -482,7 +482,12 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) - for c in "% synopsis: No space before this colon.": + for c in "% synopsis: Space before this is OK.": + qtbot.keyClick(docEditor, c, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) + + for c in "%Footnote.abc: A simple footnote.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) diff --git a/tests/tools.py b/tests/tools.py index 04d79634..504eb5b9 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -21,6 +21,7 @@ along with this program. If not, see . from __future__ import annotations import shutil +import xml.etree.ElementTree as ET from datetime import datetime from pathlib import Path @@ -110,6 +111,15 @@ def cmpFiles( return not diffFound +def xmlToText(xElem): + """Get the text content of an XML element.""" + text = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode() + bits = text.partition(">") + node = bits[0].partition(" ") + rest = " ".join(x for x in node[2].split() if not x.startswith("xmlns")).replace("/", " /") + return f"{node[0]}{rest}{bits[1]}{bits[2]}" + + def readFile(fileName: str | Path): """Returns the content of a file as a string.""" with open(fileName, mode="r", encoding="utf-8") as inFile: