From 1527937f8b869bf6c651df5e4ddffbbd00469e9f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 12 Nov 2025 10:11:38 +1100 Subject: [PATCH] Fix being able to set bold, italic and strikethrough at the same time. --- CHANGELOG.md | 1 + bouquin/markdown_highlighter.py | 81 +++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1402da5..f26637b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 0.2.1.7 + * Fix being able to set bold, italic and strikethrough at the same time. * Add AppImage # 0.2.1.6 diff --git a/bouquin/markdown_highlighter.py b/bouquin/markdown_highlighter.py index d1e82c7..34c8324 100644 --- a/bouquin/markdown_highlighter.py +++ b/bouquin/markdown_highlighter.py @@ -45,6 +45,11 @@ class MarkdownHighlighter(QSyntaxHighlighter): self.strike_format = QTextCharFormat() 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` mono = QFontDatabase.systemFont(QFontDatabase.FixedFont) self.code_format = QTextCharFormat() @@ -88,6 +93,18 @@ class MarkdownHighlighter(QSyntaxHighlighter): # Also make them very faint in case they still show 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): """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) return - # Bold: **text** or __text__ - for match in re.finditer(r"\*\*(.+?)\*\*|__(.+?)__", text): - start, end = match.span() - content_start = start + 2 - content_end = end - 2 + # Bold+Italic: ***text*** or ___text___ + # Do these first and remember their spans so later passes don't override them. + occupied = [] + for m in re.finditer( + r"(? 0 and text[start - 1 : start + 1] in ("**", "__"): continue if end < len(text) and text[end : end + 1] in ("*", "_"): continue - - content_start = start + 1 - content_end = end - 1 - - # Gray out markers + content_start, content_end = start + 1, end - 1 self.setFormat(start, 1, self.syntax_format) self.setFormat(end - 1, 1, self.syntax_format) - - # Italicize content self.setFormat( content_start, content_end - content_start, self.italic_format ) # Strikethrough: ~~text~~ - for match in re.finditer(r"~~(.+?)~~", text): - start, end = match.span() - content_start = start + 2 - content_end = end - 2 - + for m in re.finditer(r"~~(.+?)~~", text): + start, end = m.span() + content_start, content_end = start + 2, end - 2 + # Fade the markers self.setFormat(start, 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 )