Add markdown-style highlight (#2412)

This commit is contained in:
Veronica Berglyd Olsen
2025-06-16 22:13:48 +02:00
committed by GitHub
14 changed files with 112 additions and 41 deletions
+1
View File
@@ -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"
+1
View File
@@ -77,6 +77,7 @@ class nwRegEx:
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_HL = r"(?<![\w\\])(={2})(?![\s=])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_SC = r"(?i)(?<!\\)(\[(?:b|/b|i|/i|s|/s|u|/u|m|/m|sup|/sup|sub|/sub|br)\])"
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote|field):)(.+?)(?<!\\)(\])"
+30 -29
View File
@@ -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):
+9 -1
View File
@@ -29,7 +29,15 @@ from enum import Flag, IntEnum
from PyQt6.QtGui import QColor
ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""}
ESCAPES = {
r"\*": "*",
r"\~": "~",
r"\=": "=",
r"\_": "_",
r"\[": "[",
r"\]": "]",
r"\ ": "",
}
RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL)
+1
View File
@@ -189,6 +189,7 @@ class Tokenizer(ABC):
(REGEX_PATTERNS.markdownItalic, [0, TextFmt.I_B, 0, TextFmt.I_E]),
(REGEX_PATTERNS.markdownBold, [0, TextFmt.B_B, 0, TextFmt.B_E]),
(REGEX_PATTERNS.markdownStrike, [0, TextFmt.D_B, 0, TextFmt.D_E]),
(REGEX_PATTERNS.markdownMark, [0, TextFmt.M_B, 0, TextFmt.M_E]),
]
self._shortCodeFmt = {
+11
View File
@@ -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")
+16 -3
View File
@@ -110,6 +110,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._addCharFormat("bold", colEmph, "b")
self._addCharFormat("italic", colEmph, "i")
self._addCharFormat("strike", syntax.hidden, "s")
self._addCharFormat("mark", syntax.mark, "bg")
self._addCharFormat("mspaces", syntax.error, "err")
self._addCharFormat("nobreak", colBreak, "bg")
self._addCharFormat("altdialog", syntax.dialA)
@@ -196,6 +197,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
# Markdown Highlight
rxRule = REGEX_PATTERNS.markdownMark
hlRule = {
1: self._hStyles["markup"],
2: self._hStyles["mark"],
3: self._hStyles["markup"],
}
self._minRules.append((rxRule, hlRule))
self._txtRules.append((rxRule, hlRule))
self._cmnRules.append((rxRule, hlRule))
# Shortcodes
rxRule = REGEX_PATTERNS.shortcodePlain
hlRule = {
@@ -437,9 +449,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
charFormat = QTextCharFormat()
charFormat.setFontStyleName(name)
if color:
charFormat.setForeground(color)
if style:
styles = style.split(",")
if "b" in styles:
@@ -455,6 +464,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
charFormat.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SpellCheckUnderline)
if "bg" in styles and color is not None:
charFormat.setBackground(QBrush(color, Qt.BrushStyle.SolidPattern))
color = None
if color:
charFormat.setForeground(color)
if size:
charFormat.setFontPointSize(round(size*CONFIG.textFont.pointSize()))
+8
View File
@@ -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()
+6
View File
@@ -42,6 +42,7 @@ class RegExPatterns:
_rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE)
_rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE)
_rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE)
_rxMark = re.compile(nwRegEx.FMT_HL, re.UNICODE)
_rxSCPlain = re.compile(nwRegEx.FMT_SC, re.UNICODE)
_rxSCValue = re.compile(nwRegEx.FMT_SV, re.UNICODE)
@@ -75,6 +76,11 @@ class RegExPatterns:
"""Markdown strikethrough style."""
return self._rxStrike
@property
def markdownMark(self) -> re.Pattern:
"""Markdown highlight style."""
return self._rxMark
@property
def shortcodePlain(self) -> re.Pattern:
"""Plain shortcode style."""
+3 -3
View File
@@ -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 doesnt 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== doesnt 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].
+4 -4
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.8a0" hexVersion="0x020800a0" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-12 13:04:43">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2221" autoCount="289" editTime="97582">
<novelWriterXML appVersion="2.8a1" hexVersion="0x020800a1" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-16 12:51:59">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2234" autoCount="290" editTime="98009">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
@@ -36,7 +36,7 @@
<entry key="i56be10" count="1" color="purple" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="31" novelWords="1016" notesWords="416" novelChars="5602" notesChars="2285">
<content items="31" novelWords="1016" notesWords="416" novelChars="5606" notesChars="2285">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name>
@@ -58,7 +58,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="1225" />
<meta expanded="no" heading="H3" charCount="3003" wordCount="530" paraCount="16" cursorPos="1493" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
+5
View File
@@ -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."
+8 -1
View File
@@ -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
+9
View File
@@ -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():