Browse Source

fix MathJax highlight with inline code, HTML comment, link, bold, and italic elements

Le Tan 7 years ago
parent
commit
67e40cbe87

+ 324 - 75
src/hgmarkdownhighlighter.cpp

@@ -185,8 +185,13 @@ void HGMarkdownHighlighter::highlightBlock(const QString &text)
         if (isVerbatimBlock(curBlock)) {
             setCurrentBlockState(HighlightBlockState::Verbatim);
             goto exit;
-        } else if (m_enableMathjax) {
-            highlightMathJax(text);
+        }
+
+        highlightHeaderFast(blockNum, text);
+
+        if (m_enableMathjax
+            && currentBlockState() == HighlightBlockState::Normal) {
+            highlightMathJax(curBlock, text);
         }
     }
 
@@ -195,8 +200,6 @@ void HGMarkdownHighlighter::highlightBlock(const QString &text)
     // fix this.
     // highlightLinkWithSpacesInURL(text);
 
-    highlightHeaderFast(blockNum, text);
-
     if (currentBlockState() != HighlightBlockState::CodeBlock) {
         goto exit;
     }
@@ -304,7 +307,6 @@ void HGMarkdownHighlighter::initBlockHighlightFromResult(int nrBlocks)
 
 void HGMarkdownHighlighter::initHtmlCommentRegionsFromResult()
 {
-    // From Qt5.7, the capacity is preserved.
     m_commentRegions.clear();
 
     if (!result) {
@@ -318,12 +320,9 @@ void HGMarkdownHighlighter::initHtmlCommentRegionsFromResult()
             continue;
         }
 
-        m_commentRegions.push_back(VElementRegion(elem->pos, elem->end));
-
+        initBlockElementRegionOne(m_commentRegions, elem->pos, elem->end);
         elem = elem->next;
     }
-
-    qDebug() << "highlighter: parse" << m_commentRegions.size() << "HTML comment regions";
 }
 
 void HGMarkdownHighlighter::initImageRegionsFromResult()
@@ -523,7 +522,7 @@ static bool intersect(const QList<QPair<int, int>> &p_indices, int &p_start, int
     return false;
 }
 
-void HGMarkdownHighlighter::highlightMathJax(const QString &p_text)
+void HGMarkdownHighlighter::highlightMathJax(const QTextBlock &p_block, const QString &p_text)
 {
     const int blockMarkLength = 2;
     const int inlineMarkLength = 1;
@@ -531,6 +530,8 @@ void HGMarkdownHighlighter::highlightMathJax(const QString &p_text)
     VTextBlockData *blockData = currentBlockData();
     Q_ASSERT(blockData);
 
+    int blockNumber = p_block.blockNumber();
+
     int startIdx = 0;
     // Next position to search.
     int pos = 0;
@@ -542,71 +543,84 @@ void HGMarkdownHighlighter::highlightMathJax(const QString &p_text)
     // Mathjax block formula.
     if (state != HighlightBlockState::MathjaxBlock) {
         fromPreBlock = false;
-        startIdx = m_mathjaxBlockExp.indexIn(p_text);
-        pos = startIdx + m_mathjaxBlockExp.matchedLength();
-        startIdx = pos - blockMarkLength;
+        startIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxBlockExp, blockMarkLength);
+        pos = startIdx + blockMarkLength;
     }
 
     while (startIdx >= 0) {
-        int endIdx = m_mathjaxBlockExp.indexIn(p_text, pos);
+        int endIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxBlockExp, blockMarkLength);
         int mathLength = 0;
         MathjaxInfo info;
+        bool valid = false;
         if (endIdx == -1) {
-            setCurrentBlockState(HighlightBlockState::MathjaxBlock);
             mathLength = p_text.length() - startIdx;
-            pos = startIdx + mathLength;
-
-            info.m_previewedAsBlock = false;
-            info.m_index = startIdx,
-            info.m_length = mathLength;
-            if (fromPreBlock) {
-                VTextBlockData *preBlockData = previousBlockData();
-                Q_ASSERT(preBlockData);
-                const MathjaxInfo &preInfo = preBlockData->getPendingMathjax();
-                info.m_text = preInfo.text() + "\n" + p_text.mid(startIdx, mathLength);
-            } else {
-                info.m_text = p_text.mid(startIdx, mathLength);
-            }
+            pos = p_text.length();
 
-            blockData->setPendingMathjax(info);
+            if (isValidMathjaxRegion(blockNumber, startIdx, pos)) {
+                valid = true;
+                setCurrentBlockState(HighlightBlockState::MathjaxBlock);
+                info.m_previewedAsBlock = false;
+                info.m_index = startIdx,
+                info.m_length = mathLength;
+                if (fromPreBlock) {
+                    VTextBlockData *preBlockData = previousBlockData();
+                    Q_ASSERT(preBlockData);
+                    const MathjaxInfo &preInfo = preBlockData->getPendingMathjax();
+                    info.m_text = preInfo.text() + "\n" + p_text.mid(startIdx, mathLength);
+                } else {
+                    info.m_text = p_text.mid(startIdx, mathLength);
+                }
+
+                blockData->setPendingMathjax(info);
+            }
         } else {
             // Found end marker of a formula.
-            mathLength = endIdx - startIdx + m_mathjaxBlockExp.matchedLength();
-            pos = startIdx + mathLength;
-
-            info.m_previewedAsBlock = false;
-            info.m_index = startIdx;
-            info.m_length = mathLength;
-            if (fromPreBlock) {
-                // A cross-block formula.
-                if (pos >= p_text.length()) {
-                    info.m_previewedAsBlock = true;
-                }
+            pos = endIdx + blockMarkLength;
+            mathLength = pos - startIdx;
 
-                VTextBlockData *preBlockData = previousBlockData();
-                Q_ASSERT(preBlockData);
-                const MathjaxInfo &preInfo = preBlockData->getPendingMathjax();
-                info.m_text = preInfo.text() + "\n" + p_text.mid(startIdx, mathLength);
-            } else {
-                // A formula within one block.
-                if (pos >= p_text.length() && startIdx == 0) {
-                    info.m_previewedAsBlock = true;
+            if (isValidMathjaxRegion(blockNumber, startIdx, pos)) {
+                valid = true;
+                info.m_previewedAsBlock = false;
+                info.m_index = startIdx;
+                info.m_length = mathLength;
+                if (fromPreBlock) {
+                    // A cross-block formula.
+                    if (pos >= p_text.length()) {
+                        info.m_previewedAsBlock = true;
+                    }
+
+                    VTextBlockData *preBlockData = previousBlockData();
+                    Q_ASSERT(preBlockData);
+                    const MathjaxInfo &preInfo = preBlockData->getPendingMathjax();
+                    info.m_text = preInfo.text() + "\n" + p_text.mid(startIdx, mathLength);
+                } else {
+                    // A formula within one block.
+                    if (pos >= p_text.length() && startIdx == 0) {
+                        info.m_previewedAsBlock = true;
+                    }
+
+                    info.m_text = p_text.mid(startIdx, mathLength);
                 }
 
-                info.m_text = p_text.mid(startIdx, mathLength);
+                blockData->addMathjax(info);
             }
-
-            blockData->addMathjax(info);
         }
 
         fromPreBlock = false;
 
-        blockIdices.append(QPair<int, int>(startIdx, pos));
+        if (valid) {
+            blockIdices.append(QPair<int, int>(startIdx, pos));
+            setFormat(startIdx, mathLength, m_mathjaxFormat);
+            startIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxBlockExp, blockMarkLength);
+            pos = startIdx + blockMarkLength;
+        } else {
+            // Make the second mark as the first one and try again.
+            if (endIdx == -1) {
+                break;
+            }
 
-        setFormat(startIdx, mathLength, m_mathjaxFormat);
-        startIdx = m_mathjaxBlockExp.indexIn(p_text, pos);
-        pos = startIdx + m_mathjaxBlockExp.matchedLength();
-        startIdx = pos - blockMarkLength;
+            startIdx = pos - blockMarkLength;
+        }
     }
 
     // Mathjax inline formula.
@@ -615,24 +629,25 @@ void HGMarkdownHighlighter::highlightMathJax(const QString &p_text)
     fromPreBlock = true;
     if (state != HighlightBlockState::MathjaxInline) {
         fromPreBlock = false;
-        startIdx = m_mathjaxInlineExp.indexIn(p_text);
-        pos = startIdx + m_mathjaxInlineExp.matchedLength();
-        startIdx = pos - inlineMarkLength;
+        startIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxInlineExp, inlineMarkLength);
+        pos = startIdx + inlineMarkLength;
     }
 
     while (startIdx >= 0) {
-        int endIdx = m_mathjaxInlineExp.indexIn(p_text, pos);
+        int endIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxInlineExp, inlineMarkLength);
         int mathLength = 0;
+        bool valid = false;
         if (endIdx == -1) {
-            setCurrentBlockState(HighlightBlockState::MathjaxBlock);
             mathLength = p_text.length() - startIdx;
+            pos = p_text.length();
         } else {
-            mathLength = endIdx - startIdx + m_mathjaxInlineExp.matchedLength();
+            pos = endIdx + inlineMarkLength;
+            mathLength = pos - startIdx;
         }
 
-        pos = startIdx + mathLength;
+        valid = isValidMathjaxRegion(blockNumber, startIdx, pos);
         // Check if it intersect with blocks.
-        if (!intersect(blockIdices, startIdx, pos)) {
+        if (valid && !intersect(blockIdices, startIdx, pos)) {
             // A valid inline mathjax.
             MathjaxInfo info;
             if (endIdx == -1) {
@@ -769,6 +784,12 @@ void HGMarkdownHighlighter::parse(bool p_fast)
         initHeaderRegionsFromResult();
 
         initVerbatimBlocksFromResult();
+
+        initInlineCodeRegionsFromResult();
+
+        initBoldItalicRegionsFromResult();
+
+        initLinkRegionsFromResult();
     }
 
     if (result) {
@@ -989,11 +1010,12 @@ bool HGMarkdownHighlighter::isBlockInsideCommentRegion(const QTextBlock &p_block
         return false;
     }
 
-    int start = p_block.position();
-    int end = start + p_block.length();
-
-    for (auto const & reg : m_commentRegions) {
-        if (reg.contains(start) && reg.contains(end)) {
+    auto it = m_commentRegions.find(p_block.blockNumber());
+    if (it != m_commentRegions.end()) {
+        const QVector<VElementRegion> &regs = it.value();
+        if (regs.size() == 1
+            && regs[0].m_startPos == 0
+            && regs[0].m_endPos == p_block.length()) {
             return true;
         }
     }
@@ -1051,18 +1073,20 @@ bool HGMarkdownHighlighter::isValidHeader(const QString &p_text)
 
 void HGMarkdownHighlighter::highlightHeaderFast(int p_blockNumber, const QString &p_text)
 {
-    if (currentBlockState() != HighlightBlockState::Normal) {
-        return;
-    }
-
     auto it = m_headerBlocks.find(p_blockNumber);
     if (it != m_headerBlocks.end()) {
         const HeaderBlockInfo &info = it.value();
         if (!isValidHeader(p_text)) {
             // Set an empty format to clear formats. It seems to work.
             setFormat(0, p_text.size(), QTextCharFormat());
-        } else if (info.m_length < p_text.size()) {
-            setFormat(info.m_length, p_text.size() - info.m_length, m_headerStyles[info.m_level]);
+        } else {
+            if (info.m_length < p_text.size()) {
+                setFormat(info.m_length,
+                          p_text.size() - info.m_length,
+                          m_headerStyles[info.m_level]);
+            }
+
+            setCurrentBlockState(HighlightBlockState::Header);
         }
     }
 }
@@ -1092,3 +1116,228 @@ void HGMarkdownHighlighter::updateMathjaxBlocks()
 
     emit mathjaxBlocksUpdated(blocks);
 }
+
+void HGMarkdownHighlighter::initInlineCodeRegionsFromResult()
+{
+    m_inlineCodeRegions.clear();
+
+    if (!result) {
+        return;
+    }
+
+    pmh_element *elem = result[pmh_CODE];
+    while (elem != NULL) {
+        if (elem->end <= elem->pos) {
+            elem = elem->next;
+            continue;
+        }
+
+        initBlockElementRegionOne(m_inlineCodeRegions, elem->pos, elem->end);
+        elem = elem->next;
+    }
+}
+
+void HGMarkdownHighlighter::initBoldItalicRegionsFromResult()
+{
+    m_boldItalicRegions.clear();
+
+    if (!result) {
+        return;
+    }
+
+    pmh_element_type types[2] = {pmh_EMPH, pmh_STRONG};
+
+    for (int i = 0; i < 2; ++i) {
+        pmh_element *elem = result[types[i]];
+        while (elem != NULL) {
+            if (elem->end <= elem->pos) {
+                elem = elem->next;
+                continue;
+            }
+
+            initBlockElementRegionOne(m_boldItalicRegions, elem->pos, elem->end);
+            elem = elem->next;
+        }
+    }
+}
+
+void HGMarkdownHighlighter::initLinkRegionsFromResult()
+{
+    m_linkRegions.clear();
+
+    if (!result) {
+        return;
+    }
+
+    pmh_element_type types[2] = {pmh_LINK, pmh_IMAGE};
+
+    for (int i = 0; i < 2; ++i) {
+        pmh_element *elem = result[types[i]];
+        while (elem != NULL) {
+            if (elem->end <= elem->pos) {
+                elem = elem->next;
+                continue;
+            }
+
+            initBlockElementRegionOne(m_linkRegions, elem->pos, elem->end);
+            elem = elem->next;
+        }
+    }
+}
+
+void HGMarkdownHighlighter::initBlockElementRegionOne(QHash<int, QVector<VElementRegion>> &p_regs,
+                                                      unsigned long p_pos,
+                                                      unsigned long p_end)
+{
+    // When the the highlight element is at the end of document, @p_end will equals
+    // to the characterCount.
+    unsigned int nrChar = (unsigned int)document->characterCount();
+    if (p_end >= nrChar && nrChar > 0) {
+        p_end = nrChar - 1;
+    }
+
+    int startBlockNum = document->findBlock(p_pos).blockNumber();
+    int endBlockNum = document->findBlock(p_end).blockNumber();
+
+    for (int i = startBlockNum; i <= endBlockNum; ++i)
+    {
+        QTextBlock block = document->findBlockByNumber(i);
+        int blockStartPos = block.position();
+
+        QVector<VElementRegion> &regs = p_regs[i];
+        int start, end;
+        if (i == startBlockNum) {
+            start = p_pos - blockStartPos;
+            end = (startBlockNum == endBlockNum) ? (p_end - blockStartPos)
+                                                 : block.length();
+        } else if (i == endBlockNum) {
+            start = 0;
+            end = p_end - blockStartPos;
+        } else {
+            start = 0;
+            end = block.length();
+        }
+
+        regs.append(VElementRegion(start, end));
+    }
+}
+
+static bool indexInsideRegion(const QVector<VElementRegion> &p_regions, int p_idx)
+{
+    for (auto const & reg : p_regions) {
+        if (reg.contains(p_idx)) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+int HGMarkdownHighlighter::findMathjaxMarker(int p_blockNumber,
+                                             const QString &p_text,
+                                             int p_pos,
+                                             QRegExp &p_reg,
+                                             int p_markerLength)
+{
+    if (p_pos >= p_text.size()) {
+        return -1;
+    }
+
+    int idx = -1;
+    auto inlineCodeIt = m_inlineCodeRegions.find(p_blockNumber);
+    auto commentIt = m_commentRegions.find(p_blockNumber);
+    auto boldItalicIt = m_boldItalicRegions.find(p_blockNumber);
+    auto linkIt = m_linkRegions.find(p_blockNumber);
+
+    while (p_pos < p_text.size()) {
+        idx = p_reg.indexIn(p_text, p_pos);
+        if (idx == -1) {
+            return idx;
+        }
+
+        p_pos = idx + p_reg.matchedLength();
+        idx = p_pos - p_markerLength;
+
+        // Check if this idx is legal.
+        // Check inline code.
+        if (inlineCodeIt != m_inlineCodeRegions.end()) {
+            if (indexInsideRegion(inlineCodeIt.value(), idx)) {
+                idx = -1;
+                continue;
+            }
+        }
+
+        if (commentIt != m_commentRegions.end()) {
+            if (indexInsideRegion(commentIt.value(), idx)) {
+                idx = -1;
+                continue;
+            }
+        }
+
+        if (boldItalicIt != m_boldItalicRegions.end()) {
+            if (indexInsideRegion(boldItalicIt.value(), idx)) {
+                idx = -1;
+                continue;
+            }
+        }
+
+        if (linkIt != m_linkRegions.end()) {
+            if (indexInsideRegion(linkIt.value(), idx)) {
+                idx = -1;
+                continue;
+            }
+        }
+
+        break;
+    }
+
+    return idx;
+}
+
+static bool intersectRegion(const QVector<VElementRegion> &p_regions,
+                            int p_start,
+                            int p_end)
+{
+    for (auto const & reg : p_regions) {
+        if (reg.intersect(p_start, p_end)) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+bool HGMarkdownHighlighter::isValidMathjaxRegion(int p_blockNumber,
+                                                 int p_start,
+                                                 int p_end)
+{
+    auto inlineCodeIt = m_inlineCodeRegions.find(p_blockNumber);
+    if (inlineCodeIt != m_inlineCodeRegions.end()) {
+        if (intersectRegion(inlineCodeIt.value(), p_start, p_end)) {
+            return false;
+        }
+    }
+
+    auto commentIt = m_commentRegions.find(p_blockNumber);
+    if (commentIt != m_commentRegions.end()) {
+        if (intersectRegion(commentIt.value(), p_start, p_end)) {
+            return false;
+        }
+    }
+
+    auto boldItalicIt = m_boldItalicRegions.find(p_blockNumber);
+    if (boldItalicIt != m_boldItalicRegions.end()) {
+        if (intersectRegion(boldItalicIt.value(), p_start, p_end)) {
+            return false;
+        }
+    }
+
+    auto linkIt = m_linkRegions.find(p_blockNumber);
+    if (linkIt != m_linkRegions.end()) {
+        if (intersectRegion(linkIt.value(), p_start, p_end)) {
+            return false;
+        }
+    }
+
+    return true;
+}

+ 45 - 5
src/hgmarkdownhighlighter.h

@@ -148,7 +148,12 @@ struct VElementRegion
     // Whether this region contains @p_pos.
     bool contains(int p_pos) const
     {
-        return m_startPos <= p_pos && m_endPos >= p_pos;
+        return m_startPos <= p_pos && m_endPos > p_pos;
+    }
+
+    bool intersect(int p_start, int p_end) const
+    {
+        return !(p_end <= m_startPos || p_start >= m_endPos);
     }
 
     bool operator==(const VElementRegion &p_other) const
@@ -168,6 +173,11 @@ struct VElementRegion
             return false;
         }
     }
+
+    QString toString() const
+    {
+        return QString("[%1,%2)").arg(m_startPos).arg(m_endPos);
+    }
 };
 
 class HGMarkdownHighlighter : public QSyntaxHighlighter
@@ -277,9 +287,6 @@ private:
 
     int m_numOfCodeBlockHighlightsToRecv;
 
-    // All HTML comment regions.
-    QVector<VElementRegion> m_commentRegions;
-
     // All image link regions.
     QVector<VElementRegion> m_imageRegions;
 
@@ -311,6 +318,18 @@ private:
 
     bool m_enableMathjax;
 
+    // Inline code regions for each block.
+    // VElementRegion's position is relative position within a block.
+    QHash<int, QVector<VElementRegion>> m_inlineCodeRegions;
+
+    QHash<int, QVector<VElementRegion>> m_boldItalicRegions;
+
+    // Including links and images.
+    QHash<int, QVector<VElementRegion>> m_linkRegions;
+
+    // Comment regions for each block.
+    QHash<int, QVector<VElementRegion>> m_commentRegions;
+
     char *content;
     int capacity;
     pmh_element **result;
@@ -321,7 +340,7 @@ private:
 
     void highlightCodeBlock(const QTextBlock &p_block, const QString &p_text);
 
-    void highlightMathJax(const QString &p_text);
+    void highlightMathJax(const QTextBlock &p_block, const QString &p_text);
 
     // Highlight links using regular expression.
     // PEG Markdown Highlight treat URLs with spaces illegal. This function is
@@ -358,6 +377,19 @@ private:
     // Fetch all the verbatim blocks from parsing result.
     void initVerbatimBlocksFromResult();
 
+    // Fetch all the inlnie code regions from parsing result.
+    void initInlineCodeRegionsFromResult();
+
+    // Fetch all bold/italic regions from parsing result.
+    void initBoldItalicRegionsFromResult();
+
+    // Fetch all bold/italic regions from parsing result.
+    void initLinkRegionsFromResult();
+
+    void initBlockElementRegionOne(QHash<int, QVector<VElementRegion>> &p_regs,
+                                   unsigned long p_pos,
+                                   unsigned long p_end);
+
     // Whether @p_block is totally inside a HTML comment.
     bool isBlockInsideCommentRegion(const QTextBlock &p_block) const;
 
@@ -385,6 +417,14 @@ private:
     // Highlight headers using regular expression first instead of waiting for
     // another parse.
     void highlightHeaderFast(int p_blockNumber, const QString &p_text);
+
+    int findMathjaxMarker(int p_blockNumber,
+                          const QString &p_text,
+                          int p_pos,
+                          QRegExp &p_reg,
+                          int p_markerLength);
+
+    bool isValidMathjaxRegion(int p_blockNumber, int p_start, int p_end);
 };
 
 inline const QVector<VElementRegion> &HGMarkdownHighlighter::getHeaderRegions() const

+ 1 - 1
src/resources/themes/v_moonlight/v_moonlight.mdhl

@@ -38,7 +38,7 @@ color-column-foreground: eeeeee
 # [VNote] Style for preview image line
 preview-image-line-foreground: 6f5799
 # [VNote] Style for MathJax
-mathjax-foreground: ce93d8
+mathjax-foreground: 4db6ac
 
 editor-selection
 foreground: dadada

+ 1 - 1
src/resources/themes/v_native/v_native.mdhl

@@ -37,7 +37,7 @@ color-column-foreground: ffff00
 # [VNote] Style for preview image line
 preview-image-line-foreground: 9575cd
 # [VNote] Style for MathJax
-mathjax-foreground: 8e24aa
+mathjax-foreground: 00897b
 
 editor-selection
 foreground: eeeeee

+ 1 - 1
src/resources/themes/v_pure/v_pure.mdhl

@@ -38,7 +38,7 @@ color-column-foreground: ffff00
 # [VNote] Style for preview image line
 preview-image-line-foreground: 9575cd
 # [VNote] Style for MathJax
-mathjax-foreground: 8e24aa
+mathjax-foreground: 00897b
 
 editor-selection
 foreground: eeeeee

+ 4 - 1
src/vconstants.h

@@ -125,7 +125,10 @@ enum HighlightBlockState
 
     // Mathjax. It means the pending state of the block.
     MathjaxBlock,
-    MathjaxInline
+    MathjaxInline,
+
+    // Header.
+    Header
 };
 
 // Pages to open on start up.

+ 6 - 6
src/vmathjaxpreviewhelper.cpp

@@ -43,12 +43,6 @@ void VMathJaxPreviewHelper::doInit()
     m_webView->hide();
     m_webView->setFocusPolicy(Qt::NoFocus);
 
-    if (focusWid) {
-        focusWid->setFocus();
-    } else {
-        m_parentWidget->setFocus();
-    }
-
     m_webDoc = new VMathJaxWebDocument(m_webView);
     connect(m_webDoc, &VMathJaxWebDocument::mathjaxPreviewResultReady,
             this, [this](int p_identifier,
@@ -75,6 +69,12 @@ void VMathJaxPreviewHelper::doInit()
     m_webView->page()->setWebChannel(channel);
 
     m_webView->setHtml(VUtils::generateMathJaxPreviewTemplate(), QUrl("qrc:/resources"));
+
+    if (focusWid) {
+        focusWid->setFocus();
+    } else {
+        m_parentWidget->setFocus();
+    }
 }
 
 void VMathJaxPreviewHelper::previewMathJax(int p_identifier,