MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - MarkdownHighlighter.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 96.8 % 157 152
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 7 7
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 52.9 % 456 241

             Branch data     Line data    Source code
       1                 :             : #include "MarkdownHighlighter.h"
       2                 :             : 
       3                 :             : #include "ui/ThemeManager.h"
       4                 :             : 
       5                 :             : #include <QFont>
       6                 :             : 
       7                 :             : // ═══════════════════════════════════════════════════════
       8                 :             : // MarkdownHighlighter (Sprint 57)
       9                 :             : // ═══════════════════════════════════════════════════════
      10                 :             : 
      11                 :             : // 67.B3: all colors come from the ThemeManager @md_* tokens
      12                 :             : // (docs/DESIGN.md §2); formats are rebuilt on theme switch.
      13                 :         884 : static QColor mdColor(const char *token) {
      14   [ +  -  +  -  :         884 :   return QColor(ThemeManager::instance().color(QLatin1String(token)));
                   +  - ]
      15                 :             : }
      16                 :             : 
      17                 :          52 : MarkdownHighlighter::MarkdownHighlighter(QTextDocument *parent)
      18   [ +  -  +  -  :          52 :     : QSyntaxHighlighter(parent) {
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
                      - ]
      19         [ +  - ]:          52 :   setupFormats();
      20         [ +  - ]:          52 :   setupInlineRules();
      21         [ +  - ]:          52 :   connect(&ThemeManager::instance(), &ThemeManager::themeChanged, this,
      22         [ +  - ]:          52 :           [this](ThemeManager::Theme) {
      23                 :           0 :             m_inlineRules.clear();
      24                 :           0 :             setupFormats();
      25                 :           0 :             setupInlineRules();
      26                 :           0 :             rehighlight();
      27                 :           0 :           });
      28                 :          52 : }
      29                 :             : 
      30                 :          52 : void MarkdownHighlighter::setupFormats() {
      31                 :             :   // Headers
      32   [ +  -  +  - ]:          52 :   m_h1Fmt = QTextCharFormat();
      33                 :          52 :   m_h1Fmt.setFontWeight(QFont::Bold);
      34   [ +  -  +  -  :          52 :   m_h1Fmt.setForeground(mdColor("@md_heading"));
                   +  - ]
      35                 :          52 :   m_h1Fmt.setFontPointSize(18);
      36                 :             : 
      37   [ +  -  +  - ]:          52 :   m_h2Fmt = QTextCharFormat();
      38                 :          52 :   m_h2Fmt.setFontWeight(QFont::Bold);
      39   [ +  -  +  -  :          52 :   m_h2Fmt.setForeground(mdColor("@md_heading2"));
                   +  - ]
      40                 :          52 :   m_h2Fmt.setFontPointSize(15);
      41                 :             : 
      42   [ +  -  +  - ]:          52 :   m_h3Fmt = QTextCharFormat();
      43                 :          52 :   m_h3Fmt.setFontWeight(QFont::Bold);
      44   [ +  -  +  -  :          52 :   m_h3Fmt.setForeground(mdColor("@md_heading3"));
                   +  - ]
      45                 :          52 :   m_h3Fmt.setFontPointSize(13);
      46                 :             : 
      47                 :             :   // Blockquote
      48   [ +  -  +  - ]:          52 :   m_blockquoteFmt = QTextCharFormat();
      49   [ +  -  +  -  :          52 :   m_blockquoteFmt.setForeground(mdColor("@md_quote_fg"));
                   +  - ]
      50                 :          52 :   m_blockquoteFmt.setFontItalic(true);
      51                 :             : 
      52                 :             :   // List markers
      53   [ +  -  +  - ]:          52 :   m_listMarkerFmt = QTextCharFormat();
      54   [ +  -  +  -  :          52 :   m_listMarkerFmt.setForeground(mdColor("@md_list_marker"));
                   +  - ]
      55                 :          52 :   m_listMarkerFmt.setFontWeight(QFont::Bold);
      56                 :             : 
      57                 :             :   // Checkboxes
      58   [ +  -  +  - ]:          52 :   m_cbUncheckedFmt = QTextCharFormat();
      59   [ +  -  +  -  :          52 :   m_cbUncheckedFmt.setForeground(mdColor("@md_cb_unchecked"));
                   +  - ]
      60   [ +  -  +  - ]:          52 :   m_cbCheckedFmt = QTextCharFormat();
      61   [ +  -  +  -  :          52 :   m_cbCheckedFmt.setForeground(mdColor("@md_cb_checked"));
                   +  - ]
      62                 :          52 :   m_cbCheckedFmt.setFontWeight(QFont::Bold);
      63                 :             : 
      64                 :             :   // Horizontal rule
      65   [ +  -  +  - ]:          52 :   m_hrFmt = QTextCharFormat();
      66   [ +  -  +  -  :          52 :   m_hrFmt.setForeground(mdColor("@md_hr"));
                   +  - ]
      67                 :             : 
      68                 :             :   // Code block (multi-line)
      69   [ +  -  +  - ]:          52 :   m_codeBlockFmt = QTextCharFormat();
      70   [ +  -  +  -  :          52 :   m_codeBlockFmt.setForeground(mdColor("@md_code_fg"));
                   +  - ]
      71   [ +  -  +  +  :         104 :   m_codeBlockFmt.setFontFamilies({QStringLiteral("monospace")});
                   -  - ]
      72   [ +  -  +  -  :          52 :   m_codeBlockFmt.setBackground(mdColor("@md_code_bg"));
                   +  - ]
      73                 :             : 
      74                 :             :   // Code fence start/end
      75         [ +  - ]:          52 :   m_codeFenceRe = QRegularExpression(QStringLiteral("^\\s*```"));
      76   [ +  -  -  -  :         156 : }
                   -  - ]
      77                 :             : 
      78                 :          52 : void MarkdownHighlighter::setupInlineRules() {
      79                 :             :   // Bold: **text**
      80                 :             :   {
      81         [ +  - ]:          52 :     InlineRule rule;
      82         [ +  - ]:          52 :     rule.pattern = QRegularExpression(QStringLiteral("\\*\\*(.+?)\\*\\*"));
      83         [ +  - ]:          52 :     rule.format.setFontWeight(QFont::Bold);
      84   [ +  -  +  -  :          52 :     rule.format.setForeground(mdColor("@md_text"));
                   +  - ]
      85         [ +  - ]:          52 :     m_inlineRules.append(rule);
      86                 :          52 :   }
      87                 :             : 
      88                 :             :   // Italic: *text*
      89                 :             :   {
      90         [ +  - ]:          52 :     InlineRule rule;
      91                 :          52 :     rule.pattern = QRegularExpression(
      92         [ +  - ]:         104 :         QStringLiteral("(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)"));
      93         [ +  - ]:          52 :     rule.format.setFontItalic(true);
      94   [ +  -  +  -  :          52 :     rule.format.setForeground(mdColor("@md_text"));
                   +  - ]
      95         [ +  - ]:          52 :     m_inlineRules.append(rule);
      96                 :          52 :   }
      97                 :             : 
      98                 :             :   // Inline code: `code`
      99                 :             :   {
     100         [ +  - ]:          52 :     InlineRule rule;
     101         [ +  - ]:          52 :     rule.pattern = QRegularExpression(QStringLiteral("`([^`]+)`"));
     102   [ +  -  +  +  :         104 :     rule.format.setFontFamilies({QStringLiteral("monospace")});
                   -  - ]
     103   [ +  -  +  -  :          52 :     rule.format.setForeground(mdColor("@md_inline_code"));
                   +  - ]
     104   [ +  -  +  -  :          52 :     rule.format.setBackground(mdColor("@md_inline_code_bg"));
                   +  - ]
     105         [ +  - ]:          52 :     m_inlineRules.append(rule);
     106                 :          52 :   }
     107                 :             : 
     108                 :             :   // Links: [text](url)
     109                 :             :   {
     110         [ +  - ]:          52 :     InlineRule rule;
     111                 :          52 :     rule.pattern = QRegularExpression(
     112         [ +  - ]:         104 :         QStringLiteral("\\[([^\\]]+)\\]\\(([^)]+)\\)"));
     113   [ +  -  +  -  :          52 :     rule.format.setForeground(mdColor("@md_heading"));
                   +  - ]
     114         [ +  - ]:          52 :     rule.format.setFontUnderline(true);
     115         [ +  - ]:          52 :     m_inlineRules.append(rule);
     116                 :          52 :   }
     117                 :             : 
     118                 :             :   // Strikethrough: ~~text~~
     119                 :             :   {
     120         [ +  - ]:          52 :     InlineRule rule;
     121         [ +  - ]:          52 :     rule.pattern = QRegularExpression(QStringLiteral("~~(.+?)~~"));
     122         [ +  - ]:          52 :     rule.format.setFontStrikeOut(true);
     123   [ +  -  +  -  :          52 :     rule.format.setForeground(mdColor("@md_strike"));
                   +  - ]
     124         [ +  - ]:          52 :     m_inlineRules.append(rule);
     125                 :          52 :   }
     126                 :             : 
     127                 :             :   // Table pipes: |
     128                 :             :   {
     129         [ +  - ]:          52 :     InlineRule rule;
     130         [ +  - ]:          52 :     rule.pattern = QRegularExpression(QStringLiteral("\\|"));
     131   [ +  -  +  -  :          52 :     rule.format.setForeground(mdColor("@md_strike"));
                   +  - ]
     132         [ +  - ]:          52 :     rule.format.setFontWeight(QFont::Bold);
     133         [ +  - ]:          52 :     m_inlineRules.append(rule);
     134                 :          52 :   }
     135   [ +  -  -  -  :         156 : }
                   -  - ]
     136                 :             : 
     137                 :         124 : void MarkdownHighlighter::highlightInlineRules(const QString &text) {
     138   [ +  -  +  -  :         868 :   for (const auto &rule : m_inlineRules) {
                   +  + ]
     139         [ +  - ]:         744 :     auto it = rule.pattern.globalMatch(text);
     140   [ +  -  +  + ]:         757 :     while (it.hasNext()) {
     141         [ +  - ]:          13 :       auto match = it.next();
     142         [ +  - ]:          13 :       int start = match.capturedStart(rule.captureGroup);
     143         [ +  - ]:          13 :       int length = match.capturedLength(rule.captureGroup);
     144         [ +  - ]:          13 :       setFormat(start, length, rule.format);
     145                 :          13 :     }
     146                 :         744 :   }
     147                 :         124 : }
     148                 :             : 
     149                 :         137 : void MarkdownHighlighter::highlightBlock(const QString &text) {
     150                 :             :   // ── Multi-line fenced code block state machine ──
     151                 :             :   // State 0 = normal, State 1 = inside code block
     152         [ +  - ]:         137 :   int prevState = previousBlockState();
     153                 :         137 :   bool wasInCode = (prevState == 1);
     154                 :             : 
     155   [ +  -  +  -  :         137 :   if (m_codeFenceRe.match(text).hasMatch()) {
                   +  + ]
     156                 :             :     // Toggle code block state
     157         [ +  + ]:           4 :     if (wasInCode) {
     158                 :             :       // Closing fence
     159         [ +  - ]:           2 :       setFormat(0, text.length(), m_codeBlockFmt);
     160         [ +  - ]:           2 :       setCurrentBlockState(0);
     161                 :             :     } else {
     162                 :             :       // Opening fence
     163         [ +  - ]:           2 :       setFormat(0, text.length(), m_codeBlockFmt);
     164         [ +  - ]:           2 :       setCurrentBlockState(1);
     165                 :             :     }
     166                 :          26 :     return;
     167                 :             :   }
     168                 :             : 
     169         [ +  + ]:         133 :   if (wasInCode) {
     170                 :             :     // Inside code block — format entire line, no inline rules
     171         [ +  - ]:           2 :     setFormat(0, text.length(), m_codeBlockFmt);
     172         [ +  - ]:           2 :     setCurrentBlockState(1);
     173                 :           2 :     return;
     174                 :             :   }
     175                 :             : 
     176         [ +  - ]:         131 :   setCurrentBlockState(0);
     177                 :             : 
     178                 :             :   // ── Block-level rules ──
     179                 :             : 
     180                 :             :   // Headers: # H1, ## H2, ### H3+
     181   [ +  +  +  -  :         136 :   static QRegularExpression h1Re(QStringLiteral("^#{1}\\s+.+$"));
             +  -  -  - ]
     182   [ +  +  +  -  :         136 :   static QRegularExpression h2Re(QStringLiteral("^#{2}\\s+.+$"));
             +  -  -  - ]
     183   [ +  +  +  -  :         136 :   static QRegularExpression h3Re(QStringLiteral("^#{3,6}\\s+.+$"));
             +  -  -  - ]
     184                 :             : 
     185   [ +  -  +  -  :         131 :   if (h1Re.match(text).hasMatch()) {
                   +  + ]
     186         [ +  - ]:           2 :     setFormat(0, text.length(), m_h1Fmt);
     187                 :           2 :     return; // headers don't need inline rules
     188                 :             :   }
     189   [ +  -  +  -  :         129 :   if (h2Re.match(text).hasMatch()) {
                   +  + ]
     190         [ +  - ]:           2 :     setFormat(0, text.length(), m_h2Fmt);
     191                 :           2 :     return;
     192                 :             :   }
     193   [ +  -  +  -  :         127 :   if (h3Re.match(text).hasMatch()) {
                   +  + ]
     194         [ +  - ]:           1 :     setFormat(0, text.length(), m_h3Fmt);
     195                 :           1 :     return;
     196                 :             :   }
     197                 :             : 
     198                 :             :   // Horizontal rule: --- / *** / ___
     199                 :             :   static QRegularExpression hrRe(
     200   [ +  +  +  -  :         131 :       QStringLiteral("^\\s*(-{3,}|\\*{3,}|_{3,})\\s*$"));
             +  -  -  - ]
     201   [ +  -  +  -  :         126 :   if (hrRe.match(text).hasMatch()) {
                   +  + ]
     202         [ +  - ]:           2 :     setFormat(0, text.length(), m_hrFmt);
     203                 :           2 :     return;
     204                 :             :   }
     205                 :             : 
     206                 :             :   // Blockquote: > text
     207   [ +  +  +  -  :         129 :   static QRegularExpression bqRe(QStringLiteral("^>\\s?"));
             +  -  -  - ]
     208         [ +  - ]:         124 :   auto bqMatch = bqRe.match(text);
     209   [ +  -  +  + ]:         124 :   if (bqMatch.hasMatch()) {
     210         [ +  - ]:           1 :     setFormat(0, text.length(), m_blockquoteFmt);
     211                 :             :     // Still apply inline rules to quoted content
     212         [ +  - ]:           1 :     highlightInlineRules(text);
     213                 :           1 :     return;
     214                 :             :   }
     215                 :             : 
     216                 :             :   // Checkbox: - [ ] or - [x]
     217                 :             :   static QRegularExpression cbUncheckedRe(
     218   [ +  +  +  -  :         128 :       QStringLiteral("^(\\s*- \\[ \\])(.*)$"));
             +  -  -  - ]
     219                 :             :   static QRegularExpression cbCheckedRe(
     220   [ +  +  +  -  :         128 :       QStringLiteral("^(\\s*- \\[[xX]\\])(.*)$"));
             +  -  -  - ]
     221                 :             : 
     222         [ +  - ]:         123 :   auto cbChecked = cbCheckedRe.match(text);
     223   [ +  -  +  + ]:         123 :   if (cbChecked.hasMatch()) {
     224   [ +  -  +  - ]:           4 :     setFormat(0, cbChecked.capturedLength(1), m_cbCheckedFmt);
     225         [ +  - ]:           4 :     highlightInlineRules(text);
     226                 :           4 :     return;
     227                 :             :   }
     228         [ +  - ]:         119 :   auto cbUnchecked = cbUncheckedRe.match(text);
     229   [ +  -  +  + ]:         119 :   if (cbUnchecked.hasMatch()) {
     230   [ +  -  +  - ]:           8 :     setFormat(0, cbUnchecked.capturedLength(1), m_cbUncheckedFmt);
     231         [ +  - ]:           8 :     highlightInlineRules(text);
     232                 :           8 :     return;
     233                 :             :   }
     234                 :             : 
     235                 :             :   // Unordered list marker: - / * / +
     236   [ +  +  +  -  :         116 :   static QRegularExpression ulRe(QStringLiteral("^(\\s*[-*+]\\s)"));
             +  -  -  - ]
     237         [ +  - ]:         111 :   auto ulMatch = ulRe.match(text);
     238   [ +  -  +  + ]:         111 :   if (ulMatch.hasMatch()) {
     239   [ +  -  +  - ]:           5 :     setFormat(0, ulMatch.capturedLength(1), m_listMarkerFmt);
     240                 :             :   }
     241                 :             : 
     242                 :             :   // Ordered list marker: 1.
     243   [ +  +  +  -  :         116 :   static QRegularExpression olRe(QStringLiteral("^(\\s*\\d+\\.\\s)"));
             +  -  -  - ]
     244         [ +  - ]:         111 :   auto olMatch = olRe.match(text);
     245   [ +  -  +  + ]:         111 :   if (olMatch.hasMatch()) {
     246   [ +  -  +  - ]:           5 :     setFormat(0, olMatch.capturedLength(1), m_listMarkerFmt);
     247                 :             :   }
     248                 :             : 
     249                 :             :   // ── Inline rules (bold, italic, code, links, strikethrough, tables) ──
     250         [ +  - ]:         111 :   highlightInlineRules(text);
     251   [ +  +  +  +  :         144 : }
                   +  + ]
        

Generated by: LCOV version 2.0-1