From 148fc685902d524cc0be0c3960511fc28103382e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:46:37 +0200 Subject: [PATCH 1/3] Add markdown highlight format --- novelwriter/constants.py | 1 + novelwriter/formats/shared.py | 10 +++++++++- novelwriter/formats/tokenizer.py | 1 + novelwriter/gui/dochighlight.py | 19 ++++++++++++++++--- novelwriter/text/patterns.py | 6 ++++++ sample/content/636b6aa9b697b.nwd | 6 +++--- sample/nwProject.nwx | 8 ++++---- tests/test_formats/test_fmt_tokenizer.py | 5 +++++ 8 files changed, 45 insertions(+), 11 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 701ac5a1..e47455d8 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -77,6 +77,7 @@ class nwRegEx: FMT_EI = r"(? re.Pattern: + """Markdown highlight style.""" + return self._rxMark + @property def shortcodePlain(self) -> re.Pattern: """Plain shortcode style.""" diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index e3d4bfde..12f0d4e0 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,8 +1,8 @@ %%~name: Making a Scene %%~path: 6a2d6d5f4f401/636b6aa9b697b %%~kind: NOVEL/DOCUMENT -%%~hash: 1f3d98a6a27b4f9a7f2239fcd78f9e2263cd3b97 -%%~date: Unknown/2025-05-18 22:36:59 +%%~hash: b85f815702763f58926edd72a2eeed1df5e1cbc9 +%%~date: Unknown/2025-06-16 12:49:10 ### Making a Scene @pov: Jane @@ -15,7 +15,7 @@ A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. The scene document can be sorted after the chapter document, or as a child of the chapter. Both result in the same output in the end, so it is a matter of preference. -Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesn’t show it correctly, the export tool will not either. +Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the ==syntax highlighter== doesn’t show it correctly, the export tool will not either. For special formatting aside from standard emphasis, you can use “shortcodes”. Here we have super[sup]script[/sup] and sub[sub]script[/sub], and [b]part[/b]ial bold too, which ends in the middle of a word. The shortcodes also support [u]underline[/u], and you can [m]highlight text as well[/m]. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 32ed2662..587802ef 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -36,7 +36,7 @@ Main - + Novel @@ -58,7 +58,7 @@ Chapter One - + Making a Scene diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index d141b6ef..2b053e1b 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -919,6 +919,11 @@ def testFmtToken_ExtractFormats(mockGUI): assert text == "Text with strikethrough in it." assert fmt == [(10, TextFmt.D_B, ""), (23, TextFmt.D_E, "")] + # Plain mark + text, fmt = tokens._extractFormats("Text with ==a highlight== in it.") + assert text == "Text with a highlight in it." + assert fmt == [(10, TextFmt.M_B, ""), (21, TextFmt.M_E, "")] + # Nested bold/italics text, fmt = tokens._extractFormats("Text with **bold and _italics_** in it.") assert text == "Text with bold and italics in it." From c0da955fc6b55db43db969e14a943d480a6b0024 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:47:16 +0200 Subject: [PATCH 2/3] Add button and menu entry for markdown highlight --- docs/source/features/shortcuts.rst | 1 + novelwriter/enum.py | 59 ++++++++++++++-------------- novelwriter/gui/doceditor.py | 11 ++++++ novelwriter/gui/mainmenu.py | 8 ++++ tests/test_gui/test_gui_doceditor.py | 9 ++++- 5 files changed, 58 insertions(+), 30 deletions(-) diff --git a/docs/source/features/shortcuts.rst b/docs/source/features/shortcuts.rst index 0a29927b..e5811c85 100644 --- a/docs/source/features/shortcuts.rst +++ b/docs/source/features/shortcuts.rst @@ -101,6 +101,7 @@ Text Formatting Shortcuts ":kbd:`Ctrl+B`", "Format selected text, or word under cursor, with bold" ":kbd:`Ctrl+D`", "Format selected text, or word under cursor, with strike through" ":kbd:`Ctrl+I`", "Format selected text, or word under cursor, with italic" + ":kbd:`Ctrl+M`", "Highlight selected text, or word under cursor" ":kbd:`Ctrl+Shift+/`", "Remove format for block or selected text" ":kbd:`Ctrl+Shift+D`", "Toggle ignored text format for block or selected text" diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 18899295..9e50620e 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -92,35 +92,36 @@ class nwDocAction(Enum): MD_ITALIC = 6 MD_BOLD = 7 MD_STRIKE = 8 - S_QUOTE = 9 - D_QUOTE = 10 - SEL_ALL = 11 - SEL_PARA = 12 - BLOCK_H1 = 13 - BLOCK_H2 = 14 - BLOCK_H3 = 15 - BLOCK_H4 = 16 - BLOCK_COM = 17 - BLOCK_IGN = 18 - BLOCK_TXT = 19 - BLOCK_TTL = 20 - BLOCK_UNN = 21 - BLOCK_HSC = 22 - REPL_SNG = 23 - REPL_DBL = 24 - RM_BREAKS = 25 - ALIGN_L = 26 - ALIGN_C = 27 - ALIGN_R = 28 - INDENT_L = 29 - INDENT_R = 30 - SC_ITALIC = 31 - SC_BOLD = 32 - SC_STRIKE = 33 - SC_ULINE = 34 - SC_MARK = 35 - SC_SUP = 36 - SC_SUB = 37 + MD_MARK = 9 + S_QUOTE = 10 + D_QUOTE = 11 + SEL_ALL = 12 + SEL_PARA = 13 + BLOCK_H1 = 14 + BLOCK_H2 = 15 + BLOCK_H3 = 16 + BLOCK_H4 = 17 + BLOCK_COM = 18 + BLOCK_IGN = 19 + BLOCK_TXT = 20 + BLOCK_TTL = 21 + BLOCK_UNN = 22 + BLOCK_HSC = 23 + REPL_SNG = 24 + REPL_DBL = 25 + RM_BREAKS = 26 + ALIGN_L = 27 + ALIGN_C = 28 + ALIGN_R = 29 + INDENT_L = 30 + INDENT_R = 31 + SC_ITALIC = 32 + SC_BOLD = 33 + SC_STRIKE = 34 + SC_ULINE = 35 + SC_MARK = 36 + SC_SUP = 37 + SC_SUB = 38 class nwDocInsert(Enum): diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 5e49d67e..9f0f7bfd 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -286,6 +286,7 @@ class GuiDocEditor(QPlainTextEdit): self.docHeader.clearHeader() self.docFooter.setHandle(self._docHandle) self.docToolBar.setVisible(False) + self.setExtraSelections([]) self.itemHandleChanged.emit("") @@ -742,6 +743,8 @@ class GuiDocEditor(QPlainTextEdit): self._toggleFormat(2, "*") elif action == nwDocAction.MD_STRIKE: self._toggleFormat(2, "~") + elif action == nwDocAction.MD_MARK: + self._toggleFormat(2, "=") elif action == nwDocAction.S_QUOTE: self._wrapSelection(CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose) elif action == nwDocAction.D_QUOTE: @@ -2418,6 +2421,12 @@ class GuiDocToolBar(QWidget): qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE) ) + self.tbMarkMD = NIconToolButton(self, iSz) + self.tbMarkMD.setToolTip(self.tr("Markdown Highlight")) + self.tbMarkMD.clicked.connect( + qtLambda(self.requestDocAction.emit, nwDocAction.MD_MARK) + ) + self.tbBold = NIconToolButton(self, iSz) self.tbBold.setToolTip(self.tr("Shortcode Bold")) self.tbBold.clicked.connect( @@ -2467,6 +2476,7 @@ class GuiDocToolBar(QWidget): self.outerBox.addWidget(self.tbBoldMD) self.outerBox.addWidget(self.tbItalicMD) self.outerBox.addWidget(self.tbStrikeMD) + self.outerBox.addWidget(self.tbMarkMD) self.outerBox.addSpacing(4) self.outerBox.addWidget(self.tbBold) self.outerBox.addWidget(self.tbItalic) @@ -2501,6 +2511,7 @@ class GuiDocToolBar(QWidget): self.tbBoldMD.setThemeIcon("fmt_bold", "orange") self.tbItalicMD.setThemeIcon("fmt_italic", "orange") self.tbStrikeMD.setThemeIcon("fmt_strike", "orange") + self.tbMarkMD.setThemeIcon("fmt_mark", "orange") self.tbBold.setThemeIcon("fmt_bold") self.tbItalic.setThemeIcon("fmt_italic") self.tbStrike.setThemeIcon("fmt_strike") diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 33f97dde..3d0ab1c5 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -675,6 +675,14 @@ class GuiMainMenu(QMenuBar): ) self.mainGui.addAction(self.aFmtStrike) + # Format > Highlight + self.aFmtMark = qtAddAction(self.fmtMenu, self.tr("Highlight")) + self.aFmtMark.setShortcut("Ctrl+M") + self.aFmtMark.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.MD_MARK) + ) + self.mainGui.addAction(self.aFmtStrike) + # Edit > Separator self.fmtMenu.addSeparator() diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 5bda2d82..ea8f86dd 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -674,9 +674,16 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text + # Mark + docEditor.setCursorPosition(50) + assert docEditor.docAction(nwDocAction.MD_MARK) is True + assert docEditor.getText() == text.replace("consectetur", "==consectetur==") + assert docEditor.docAction(nwDocAction.UNDO) is True + assert docEditor.getText() == text + # Redo assert docEditor.docAction(nwDocAction.REDO) is True - assert docEditor.getText() == text.replace("consectetur", "~~consectetur~~") + assert docEditor.getText() == text.replace("consectetur", "==consectetur==") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text From 0b13d4051637b83d9a306285562b3723dfa3b5a5 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 16 Jun 2025 21:46:27 +0200 Subject: [PATCH 3/3] Add text pattern test --- tests/test_text/test_text_patterns.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_text/test_text_patterns.py b/tests/test_text/test_text_patterns.py index e6a8b599..2670401e 100644 --- a/tests/test_text/test_text_patterns.py +++ b/tests/test_text/test_text_patterns.py @@ -157,6 +157,15 @@ def testTextPatterns_Markdown(): assert allMatches(regEx, "one ~two~~ three") == [] assert allMatches(regEx, "one~~two~~three") == [] + # Mark + regEx = REGEX_PATTERNS.markdownMark + assert allMatches(regEx, "one ==two== three") == [ + [("==two==", 4, 11), ("==", 4, 6), ("two", 6, 9), ("==", 9, 11)] + ] + assert allMatches(regEx, "one ==two= three") == [] + assert allMatches(regEx, "one =two== three") == [] + assert allMatches(regEx, "one==two==three") == [] + @pytest.mark.core def testTextPatterns_ShortcodesPlain():