From ff7404d01d4f7d78cd5b362f92fe6ac4f99351d1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:07:03 +0200 Subject: [PATCH] Add line break shortcode support --- novelwriter/constants.py | 4 +++- novelwriter/enum.py | 1 + novelwriter/formats/tokenizer.py | 12 ++++-------- novelwriter/gui/doceditor.py | 2 ++ novelwriter/gui/mainmenu.py | 10 ++++++++-- novelwriter/text/patterns.py | 6 ++++++ tests/test_gui/test_gui_mainmenu.py | 9 +++++++-- tests/test_text/test_text_counting.py | 5 +++-- tests/test_text/test_text_patterns.py | 11 +++++++++++ 9 files changed, 45 insertions(+), 15 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 03886a57..9451ea7f 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -61,10 +61,11 @@ class nwConst: class nwRegEx: WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b" + BREAK = r"(?i)(? 0: if end > pos: result = result[:pos] + result[end:] - formats = [(p+pos-end if p > pos else p, f, k) for p, f, k in formats] - formats.insert(0, (pos, fmt, key)) + formats = [(p+pos-end if p > pos else p, f, m) for p, f, m in formats] + formats.insert(0, (pos, fmt, meta)) return result, formats diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index a29fe34b..bedb45da 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -877,6 +877,8 @@ class GuiDocEditor(QPlainTextEdit): after = False elif insert == nwDocInsert.FOOTNOTE: self._insertCommentStructure(nwComment.FOOTNOTE) + elif insert == nwDocInsert.LINE_BRK: + text = nwShortcode.BREAK if text: if block: diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 5205b21a..3dcdfbcf 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -570,8 +570,8 @@ class GuiMainMenu(QMenuBar): lambda: self.requestDocInsert.emit(nwDocInsert.SHORT) ) - # Insert > Symbols - self.mInsBreaks = self.insMenu.addMenu(self.tr("Page Break and Space")) + # Insert > Breaks and Vertical Space + self.mInsBreaks = self.insMenu.addMenu(self.tr("Breaks and Vertical Space")) # Insert > New Page self.aInsNewPage = self.mInsBreaks.addAction(self.tr("Page Break")) @@ -579,6 +579,12 @@ class GuiMainMenu(QMenuBar): lambda: self.requestDocInsert.emit(nwDocInsert.NEW_PAGE) ) + # Insert > Forced Line Break + self.aInsLineBreak = self.mInsBreaks.addAction(self.tr("Forced Line Break")) + self.aInsLineBreak.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.LINE_BRK) + ) + # Insert > Vertical Space (Single) self.aInsVSpaceS = self.mInsBreaks.addAction(self.tr("Vertical Space (Single)")) self.aInsVSpaceS.triggered.connect( diff --git a/novelwriter/text/patterns.py b/novelwriter/text/patterns.py index c3a7cf54..faf39679 100644 --- a/novelwriter/text/patterns.py +++ b/novelwriter/text/patterns.py @@ -33,6 +33,7 @@ class RegExPatterns: # Static RegExes _rxWords = re.compile(nwRegEx.WORDS, re.UNICODE) + _rxBreak = re.compile(nwRegEx.BREAK, re.UNICODE) _rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE) _rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE) _rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE) @@ -44,6 +45,11 @@ class RegExPatterns: """Split text into words.""" return self._rxWords + @property + def lineBreak(self) -> re.Pattern: + """Find forced line break.""" + return self._rxBreak + @property def markdownItalic(self) -> re.Pattern: """Markdown italic style.""" diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 60947af0..5f9801f0 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -522,13 +522,18 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd nwGUI.mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger) assert nwGUI.docEditor.getText() == "Stuff\n%Short: \n" - # Insert Break or Space - # ===================== + # Breaks and Vertical Space + # ========================= nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger) assert nwGUI.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" + nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger) assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n" diff --git a/tests/test_text/test_text_counting.py b/tests/test_text/test_text_counting.py index 360070bd..228c017e 100644 --- a/tests/test_text/test_text_counting.py +++ b/tests/test_text/test_text_counting.py @@ -47,6 +47,7 @@ def testTextCounting_preProcessText(): "[vspace:3]\n\n" "[New Page]\n\n" "[footnote:abcd]\n\n" + "[br]\n\n" "Dashes\u2013and even longer\u2014dashes.\n\n" ) @@ -60,7 +61,7 @@ def testTextCounting_preProcessText(): "#### Heading Four", "", "", "", "A paragraph.", "", - "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "Dashes and even longer dashes.", "" ] @@ -68,7 +69,7 @@ def testTextCounting_preProcessText(): assert preProcessText(text, keepHeaders=False) == [ "", "", "", "A paragraph.", "", - "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "Dashes and even longer dashes.", "" ] diff --git a/tests/test_text/test_text_patterns.py b/tests/test_text/test_text_patterns.py index 34610b46..8877cc0c 100644 --- a/tests/test_text/test_text_patterns.py +++ b/tests/test_text/test_text_patterns.py @@ -198,6 +198,17 @@ def testTextPatterns_ShortcodesPlain(): assert allMatches(regEx, "one [x]two[/x] three") == [] + # Line Break Substitution + # ======================= + regEx = REGEX_PATTERNS.lineBreak + + assert regEx.sub("\n", "one[br]two") == "one\ntwo" + assert regEx.sub("\n", "one[br]\ntwo") == "one\ntwo" + assert regEx.sub("\n", "one[br]\n\ntwo") == "one\n\ntwo" + assert regEx.sub("\n", "one[BR]two") == "one\ntwo" + assert regEx.sub("\n", "one[BR]\ntwo") == "one\ntwo" + assert regEx.sub("\n", "one[BR]\n\ntwo") == "one\n\ntwo" + @pytest.mark.core def testTextPatterns_ShortcodesValue():