Fix being able to set bold, italic and strikethrough at the same time.

This commit is contained in:
Miguel Jacq 2025-11-12 10:11:38 +11:00
parent 37332b5618
commit 1527937f8b
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
2 changed files with 57 additions and 25 deletions

View file

@ -1,5 +1,6 @@
# 0.2.1.7 # 0.2.1.7
* Fix being able to set bold, italic and strikethrough at the same time.
* Add AppImage * Add AppImage
# 0.2.1.6 # 0.2.1.6

View file

@ -45,6 +45,11 @@ class MarkdownHighlighter(QSyntaxHighlighter):
self.strike_format = QTextCharFormat() self.strike_format = QTextCharFormat()
self.strike_format.setFontStrikeOut(True) self.strike_format.setFontStrikeOut(True)
# Allow combination of bold/italic
self.bold_italic_format = QTextCharFormat()
self.bold_italic_format.setFontWeight(QFont.Weight.Bold)
self.bold_italic_format.setFontItalic(True)
# Inline code: `code` # Inline code: `code`
mono = QFontDatabase.systemFont(QFontDatabase.FixedFont) mono = QFontDatabase.systemFont(QFontDatabase.FixedFont)
self.code_format = QTextCharFormat() self.code_format = QTextCharFormat()
@ -88,6 +93,18 @@ class MarkdownHighlighter(QSyntaxHighlighter):
# Also make them very faint in case they still show # Also make them very faint in case they still show
self.syntax_format.setForeground(QColor(250, 250, 250)) self.syntax_format.setForeground(QColor(250, 250, 250))
def _overlay_range(
self, start: int, length: int, overlay_fmt: QTextCharFormat
) -> None:
"""Merge overlay_fmt onto the existing format for each char in [start, start+length)."""
end = start + length
i = start
while i < end:
base = QTextCharFormat(self.format(i)) # current format at this position
base.merge(overlay_fmt) # add only the properties we set
self.setFormat(i, 1, base) # write back one char
i += 1
def highlightBlock(self, text: str): def highlightBlock(self, text: str):
"""Apply formatting to a block of text based on markdown syntax.""" """Apply formatting to a block of text based on markdown syntax."""
@ -141,51 +158,65 @@ class MarkdownHighlighter(QSyntaxHighlighter):
self.setFormat(marker_len, len(text) - marker_len, heading_fmt) self.setFormat(marker_len, len(text) - marker_len, heading_fmt)
return return
# Bold: **text** or __text__ # Bold+Italic: ***text*** or ___text___
for match in re.finditer(r"\*\*(.+?)\*\*|__(.+?)__", text): # Do these first and remember their spans so later passes don't override them.
start, end = match.span() occupied = []
content_start = start + 2 for m in re.finditer(
content_end = end - 2 r"(?<!\*)\*\*\*(.+?)(?<!\*)\*\*\*|(?<!_)___(.+?)(?<!_)___", text
):
start, end = m.span()
content_start, content_end = start + 3, end - 3
self.setFormat(start, 3, self.syntax_format) # hide leading ***
self.setFormat(end - 3, 3, self.syntax_format) # hide trailing ***
self.setFormat(
content_start, content_end - content_start, self.bold_italic_format
)
occupied.append((start, end))
# Gray out the markers def _overlaps(a, b):
return not (a[1] <= b[0] or b[1] <= a[0])
# Bold: **text** or __text__ (but not part of *** or ___)
for m in re.finditer(
r"(?<!\*)\*\*(?!\*)(.+?)(?<!\*)\*\*(?!\*)|(?<!_)__(?!_)(.+?)(?<!_)__(?!_)",
text,
):
start, end = m.span()
if any(_overlaps((start, end), occ) for occ in occupied):
continue
content_start, content_end = start + 2, end - 2
self.setFormat(start, 2, self.syntax_format) self.setFormat(start, 2, self.syntax_format)
self.setFormat(end - 2, 2, self.syntax_format) self.setFormat(end - 2, 2, self.syntax_format)
# Bold the content
self.setFormat(content_start, content_end - content_start, self.bold_format) self.setFormat(content_start, content_end - content_start, self.bold_format)
# Italic: *text* or _text_ (but not part of bold) # Italic: *text* or _text_ (but not part of bold/** and not inside *** or ___)
for match in re.finditer( for m in re.finditer(
r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)", text r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)", text
): ):
start, end = match.span() start, end = m.span()
# Skip if this is part of a bold pattern if any(_overlaps((start, end), occ) for occ in occupied):
continue
# Keep your existing guards that avoid grabbing * from **:
if start > 0 and text[start - 1 : start + 1] in ("**", "__"): if start > 0 and text[start - 1 : start + 1] in ("**", "__"):
continue continue
if end < len(text) and text[end : end + 1] in ("*", "_"): if end < len(text) and text[end : end + 1] in ("*", "_"):
continue continue
content_start, content_end = start + 1, end - 1
content_start = start + 1
content_end = end - 1
# Gray out markers
self.setFormat(start, 1, self.syntax_format) self.setFormat(start, 1, self.syntax_format)
self.setFormat(end - 1, 1, self.syntax_format) self.setFormat(end - 1, 1, self.syntax_format)
# Italicize content
self.setFormat( self.setFormat(
content_start, content_end - content_start, self.italic_format content_start, content_end - content_start, self.italic_format
) )
# Strikethrough: ~~text~~ # Strikethrough: ~~text~~
for match in re.finditer(r"~~(.+?)~~", text): for m in re.finditer(r"~~(.+?)~~", text):
start, end = match.span() start, end = m.span()
content_start = start + 2 content_start, content_end = start + 2, end - 2
content_end = end - 2 # Fade the markers
self.setFormat(start, 2, self.syntax_format) self.setFormat(start, 2, self.syntax_format)
self.setFormat(end - 2, 2, self.syntax_format) self.setFormat(end - 2, 2, self.syntax_format)
self.setFormat( # Merge strikeout with whatever is already applied (bold, italic, both, links, etc.)
self._overlay_range(
content_start, content_end - content_start, self.strike_format content_start, content_end - content_start, self.strike_format
) )