Sfoglia il codice sorgente

support code block syntax highlihgt in edit mode

In edit mode, highlight code blocks via parsing the result of HighlightJS.
We only highlight fenced code blocks by token-matching.

Support custom style in MDHL file.
Le Tan 8 anni fa
parent
commit
44257913f7

+ 170 - 13
src/hgmarkdownhighlighter.cpp

@@ -1,6 +1,11 @@
 #include <QtGui>
 #include <QtDebug>
+#include <QTextCursor>
+#include <algorithm>
 #include "hgmarkdownhighlighter.h"
+#include "vconfigmanager.h"
+
+extern VConfigManager vconfig;
 
 const int HGMarkdownHighlighter::initCapacity = 1024;
 
@@ -18,13 +23,16 @@ void HGMarkdownHighlighter::resizeBuffer(int newCap)
 }
 
 // Will be freeed by parent automatically
-HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles, int waitInterval,
+HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles,
+                                             const QMap<QString, QTextCharFormat> &codeBlockStyles,
+                                             int waitInterval,
                                              QTextDocument *parent)
-    : QSyntaxHighlighter(parent), parsing(0),
-      waitInterval(waitInterval), content(NULL), capacity(0), result(NULL)
+    : QSyntaxHighlighter(parent), highlightingStyles(styles),
+      m_codeBlockStyles(codeBlockStyles), m_numOfCodeBlockHighlightsToRecv(0),
+      parsing(0), waitInterval(waitInterval), content(NULL), capacity(0), result(NULL)
 {
-    codeBlockStartExp = QRegExp("^(\\s)*```");
-    codeBlockEndExp = QRegExp("^(\\s)*```$");
+    codeBlockStartExp = QRegExp("^\\s*```(\\S*)");
+    codeBlockEndExp = QRegExp("^\\s*```$");
     codeBlockFormat.setForeground(QBrush(Qt::darkYellow));
     for (int index = 0; index < styles.size(); ++index) {
         const pmh_element_type &eleType = styles[index].type;
@@ -38,7 +46,6 @@ HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector<HighlightingStyle> &s
     }
 
     resizeBuffer(initCapacity);
-    setStyles(styles);
     document = parent;
     timer = new QTimer(this);
     timer->setSingleShot(true);
@@ -65,10 +72,10 @@ void HGMarkdownHighlighter::highlightBlock(const QString &text)
 {
     int blockNum = currentBlock().blockNumber();
     if (!parsing && blockHighlights.size() > blockNum) {
-        QVector<HLUnit> &units = blockHighlights[blockNum];
+        const QVector<HLUnit> &units = blockHighlights[blockNum];
         for (int i = 0; i < units.size(); ++i) {
             // TODO: merge two format within the same range
-            const HLUnit& unit = units[i];
+            const HLUnit &unit = units[i];
             setFormat(unit.start, unit.length, highlightingStyles[unit.styleIndex].format);
         }
     }
@@ -82,11 +89,40 @@ void HGMarkdownHighlighter::highlightBlock(const QString &text)
 
     // PEG Markdown Highlight does not handle links with spaces in the URL.
     highlightLinkWithSpacesInURL(text);
-}
 
-void HGMarkdownHighlighter::setStyles(const QVector<HighlightingStyle> &styles)
-{
-    this->highlightingStyles = styles;
+    // Highlight CodeBlock using VCodeBlockHighlightHelper.
+    if (m_codeBlockHighlights.size() > blockNum) {
+        const QVector<HLUnitStyle> &units = m_codeBlockHighlights[blockNum];
+        // Manually simply merge the format of all the units within the same block.
+        // Using QTextCursor to get the char format after setFormat() seems
+        // not to work.
+        QVector<QTextCharFormat> formats;
+        formats.reserve(units.size());
+        // formatIndex[i] is the index in @formats which is the format of the
+        // ith character.
+        QVector<int> formatIndex(currentBlock().length(), -1);
+        for (int i = 0; i < units.size(); ++i) {
+            const HLUnitStyle &unit = units[i];
+            auto it = m_codeBlockStyles.find(unit.style);
+            if (it != m_codeBlockStyles.end()) {
+                QTextCharFormat newFormat;
+                if (unit.start < (unsigned int)formatIndex.size() && formatIndex[unit.start] != -1) {
+                    newFormat = formats[formatIndex[unit.start]];
+                    newFormat.merge(*it);
+                } else {
+                    newFormat = *it;
+                }
+                setFormat(unit.start, unit.length, newFormat);
+
+                formats.append(newFormat);
+                int idx = formats.size() - 1;
+                unsigned int endIdx = unit.length + unit.start;
+                for (unsigned int i = unit.start; i < endIdx && i < (unsigned int)formatIndex.size(); ++i) {
+                    formatIndex[i] = idx;
+                }
+            }
+        }
+    }
 }
 
 void HGMarkdownHighlighter::initBlockHighlightFromResult(int nrBlocks)
@@ -286,7 +322,9 @@ void HGMarkdownHighlighter::handleContentChange(int /* position */, int charsRem
 void HGMarkdownHighlighter::timerTimeout()
 {
     parse();
-    rehighlight();
+    if (!updateCodeBlocks()) {
+        rehighlight();
+    }
     emit highlightCompleted();
 }
 
@@ -295,3 +333,122 @@ void HGMarkdownHighlighter::updateHighlight()
     timer->stop();
     timerTimeout();
 }
+
+bool HGMarkdownHighlighter::updateCodeBlocks()
+{
+    if (!vconfig.getEnableCodeBlockHighlight()) {
+        m_codeBlockHighlights.clear();
+        return false;
+    }
+
+    m_codeBlockHighlights.resize(document->blockCount());
+    for (int i = 0; i < m_codeBlockHighlights.size(); ++i) {
+        m_codeBlockHighlights[i].clear();
+    }
+
+    QList<VCodeBlock> codeBlocks;
+
+    VCodeBlock item;
+    bool inBlock = false;
+
+    // Only handle complete codeblocks.
+    QTextBlock block = document->firstBlock();
+    while (block.isValid()) {
+        QString text = block.text();
+        if (inBlock) {
+            item.m_text = item.m_text + "\n" + text;
+            int idx = codeBlockEndExp.indexIn(text);
+            if (idx >= 0) {
+                // End block.
+                inBlock = false;
+                item.m_endBlock = block.blockNumber();
+                codeBlocks.append(item);
+            }
+        } else {
+            int idx = codeBlockStartExp.indexIn(text);
+            if (idx >= 0) {
+                // Start block.
+                inBlock = true;
+                item.m_startBlock = block.blockNumber();
+                item.m_startPos = block.position();
+                item.m_text = text;
+                if (codeBlockStartExp.captureCount() == 1) {
+                    item.m_lang = codeBlockStartExp.capturedTexts()[1];
+                }
+            }
+        }
+        block = block.next();
+    }
+
+    m_numOfCodeBlockHighlightsToRecv = codeBlocks.size();
+    if (m_numOfCodeBlockHighlightsToRecv > 0) {
+        emit codeBlocksUpdated(codeBlocks);
+        return true;
+    } else {
+        return false;
+    }
+}
+
+static bool HLUnitStyleComp(const HLUnitStyle &a, const HLUnitStyle &b)
+{
+    if (a.start < b.start) {
+        return true;
+    } else if (a.start == b.start) {
+        return a.length > b.length;
+    } else {
+        return false;
+    }
+}
+
+void HGMarkdownHighlighter::setCodeBlockHighlights(const QList<HLUnitPos> &p_units)
+{
+    if (p_units.isEmpty()) {
+        goto exit;
+    }
+
+    {
+    QVector<QVector<HLUnitStyle>> highlights(m_codeBlockHighlights.size());
+
+    for (auto const &unit : p_units) {
+        int pos = unit.m_position;
+        int end = unit.m_position + unit.m_length;
+        int startBlockNum = document->findBlock(pos).blockNumber();
+        int endBlockNum = document->findBlock(end).blockNumber();
+        for (int i = startBlockNum; i <= endBlockNum; ++i)
+        {
+            QTextBlock block = document->findBlockByNumber(i);
+            int blockStartPos = block.position();
+            HLUnitStyle hl;
+            hl.style = unit.m_style;
+            if (i == startBlockNum) {
+                hl.start = pos - blockStartPos;
+                hl.length = (startBlockNum == endBlockNum) ?
+                                (end - pos) : (block.length() - hl.start);
+            } else if (i == endBlockNum) {
+                hl.start = 0;
+                hl.length = end - blockStartPos;
+            } else {
+                hl.start = 0;
+                hl.length = block.length();
+            }
+
+            highlights[i].append(hl);
+        }
+    }
+
+    // Need to highlight in order.
+    for (int i = 0; i < highlights.size(); ++i) {
+        QVector<HLUnitStyle> &units = highlights[i];
+        if (!units.isEmpty()) {
+            std::sort(units.begin(), units.end(), HLUnitStyleComp);
+            m_codeBlockHighlights[i].append(units);
+        }
+    }
+    }
+
+exit:
+    --m_numOfCodeBlockHighlightsToRecv;
+    if (m_numOfCodeBlockHighlightsToRecv <= 0) {
+        rehighlight();
+    }
+}

+ 59 - 3
src/hgmarkdownhighlighter.h

@@ -5,6 +5,9 @@
 #include <QSyntaxHighlighter>
 #include <QAtomicInt>
 #include <QSet>
+#include <QList>
+#include <QString>
+#include <QMap>
 
 extern "C" {
 #include <pmh_parser.h>
@@ -38,25 +41,65 @@ struct HLUnit
     unsigned int styleIndex;
 };
 
+struct HLUnitStyle
+{
+    unsigned long start;
+    unsigned long length;
+    QString style;
+};
+
+// Fenced code block only.
+struct VCodeBlock
+{
+    int m_startPos;
+    int m_startBlock;
+    int m_endBlock;
+    QString m_lang;
+
+    QString m_text;
+};
+
+// Highlight unit with global position and string style name.
+struct HLUnitPos
+{
+    HLUnitPos() : m_position(-1), m_length(-1)
+    {
+    }
+
+    HLUnitPos(int p_position, int p_length, const QString &p_style)
+        : m_position(p_position), m_length(p_length), m_style(p_style)
+    {
+    }
+
+    int m_position;
+    int m_length;
+    QString m_style;
+};
+
 class HGMarkdownHighlighter : public QSyntaxHighlighter
 {
     Q_OBJECT
 
 public:
-    HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles, int waitInterval,
+    HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles,
+                          const QMap<QString, QTextCharFormat> &codeBlockStyles,
+                          int waitInterval,
                           QTextDocument *parent = 0);
     ~HGMarkdownHighlighter();
-    void setStyles(const QVector<HighlightingStyle> &styles);
     // Request to update highlihgt (re-parse and re-highlight)
-    void updateHighlight();
+    void setCodeBlockHighlights(const QList<HLUnitPos> &p_units);
 
 signals:
     void highlightCompleted();
     void imageBlocksUpdated(QSet<int> p_blocks);
+    void codeBlocksUpdated(const QList<VCodeBlock> &p_codeBlocks);
 
 protected:
     void highlightBlock(const QString &text) Q_DECL_OVERRIDE;
 
+public slots:
+    void updateHighlight();
+
 private slots:
     void handleContentChange(int position, int charsRemoved, int charsAdded);
     void timerTimeout();
@@ -70,7 +113,17 @@ private:
 
     QTextDocument *document;
     QVector<HighlightingStyle> highlightingStyles;
+    QMap<QString, QTextCharFormat> m_codeBlockStyles;
     QVector<QVector<HLUnit> > blockHighlights;
+
+    // Use another member to store the codeblocks highlights, because the highlight
+    // sequence is blockHighlights, regular-expression-based highlihgts, and then
+    // codeBlockHighlights.
+    // Support fenced code block only.
+    QVector<QVector<HLUnitStyle> > m_codeBlockHighlights;
+
+    int m_numOfCodeBlockHighlightsToRecv;
+
     // Block numbers containing image link(s).
     QSet<int> imageBlocks;
     QAtomicInt parsing;
@@ -92,6 +145,9 @@ private:
     void initBlockHighlihgtOne(unsigned long pos, unsigned long end,
                                int styleIndex);
     void updateImageBlocks();
+    // Return true if there are fenced code blocks and it will call rehighlight() later.
+    // Return false if there is none.
+    bool updateCodeBlocks();
 };
 
 #endif

+ 12 - 0
src/resources/hoedown.js

@@ -1,5 +1,12 @@
 var placeholder = document.getElementById('placeholder');
 
+// Use Marked to highlight code blocks.
+marked.setOptions({
+    highlight: function(code) {
+        return hljs.highlightAuto(code).value;
+    }
+});
+
 var updateHtml = function(html) {
     placeholder.innerHTML = html;
     var codes = document.getElementsByTagName('code');
@@ -45,3 +52,8 @@ var updateHtml = function(html) {
     }
 };
 
+var highlightText = function(text, id, timeStamp) {
+    var html = marked(text);
+    content.highlightTextCB(html, id, timeStamp);
+}
+

+ 5 - 0
src/resources/markdown-it.js

@@ -178,3 +178,8 @@ var updateText = function(text) {
     }
 };
 
+var highlightText = function(text, id, timeStamp) {
+    var html = mdit.render(text);
+    content.highlightTextCB(html, id, timeStamp);
+}
+

+ 17 - 12
src/resources/markdown_template.js

@@ -1,6 +1,19 @@
 var content;
 var keyState = 0;
 
+var VMermaidDivClass = 'mermaid-diagram';
+if (typeof VEnableMermaid == 'undefined') {
+    VEnableMermaid = false;
+} else if (VEnableMermaid) {
+    mermaidAPI.initialize({
+        startOnLoad: false
+    });
+}
+
+if (typeof VEnableMathjax == 'undefined') {
+    VEnableMathjax = false;
+}
+
 new QWebChannel(qt.webChannelTransport,
     function(channel) {
         content = channel.objects.content;
@@ -13,20 +26,12 @@ new QWebChannel(qt.webChannelTransport,
             content.updateText();
         }
         content.requestScrollToAnchor.connect(scrollToAnchor);
-    });
 
-var VMermaidDivClass = 'mermaid-diagram';
-if (typeof VEnableMermaid == 'undefined') {
-    VEnableMermaid = false;
-} else if (VEnableMermaid) {
-    mermaidAPI.initialize({
-        startOnLoad: false
+        if (typeof highlightText == "function") {
+            content.requestHighlightText.connect(highlightText);
+            content.noticeReadyToHighlightText();
+        }
     });
-}
-
-if (typeof VEnableMathjax == 'undefined') {
-    VEnableMathjax = false;
-}
 
 var scrollToAnchor = function(anchor) {
     var anc = document.getElementById(anchor);

+ 5 - 0
src/resources/marked.js

@@ -131,3 +131,8 @@ var updateText = function(text) {
     }
 };
 
+var highlightText = function(text, id, timeStamp) {
+    var html = marked(text);
+    content.highlightTextCB(html, id, timeStamp);
+}
+

+ 36 - 1
src/resources/styles/default.mdhl

@@ -90,8 +90,43 @@ COMMENT
 foreground: 93a1a1
 
 VERBATIM
-foreground: 551A8B
+foreground: 551a8b
 font-family: Consolas, Monaco, Andale Mono, Monospace, Courier New
+# Codeblock sylte from HighlightJS (bold, italic, underlined, color)
+# The last occurence of the same attribute takes effect
+hljs-comment: 888888
+hljs-keyword: bold
+hljs-attribute: bold
+hljs-selector-tag: bold
+hljs-meta-keyword: bold
+hljs-doctag: bold
+hljs-name: bold
+hljs-type: 880000
+hljs-string: 880000
+hljs-number: 880000
+hljs-selector-id: 880000
+hljs-selector-class: 880000
+hljs-quote: 880000
+hljs-template-tag: 880000
+hljs-deletion: 880000
+hljs-title: bold, 880000
+hljs-section: bold, 880000
+hljs-regexp: bc6060
+hljs-symbol: bc6060
+hljs-variable: bc6060
+hljs-template-variable: bc6060
+hljs-link: bc6060
+hljs-selector-attr: bc6060
+hljs-selector-pseudo: bc6060
+hljs-literal: 78a960
+hljs-built_in: 397300
+hljs-bullet: 397300
+hljs-code: 397300
+hljs-addition: 397300
+hljs-meta: 1f7199
+hljs-meta-string: 4d99bf
+hljs-emphasis: italic
+hljs-strong: bold
 
 BLOCKQUOTE
 foreground: 00af00

+ 2 - 0
src/resources/vnote.ini

@@ -19,6 +19,8 @@ enable_mermaid=false
 enable_mathjax=false
 ; -1 - calculate the factor
 web_zoom_factor=-1
+; Syntax highlight within code blocks in edit mode
+enable_code_block_highlight=false
 
 [session]
 tools_dock_checked=true

+ 4 - 2
src/src.pro

@@ -58,7 +58,8 @@ SOURCES += main.cpp\
     dialog/vselectdialog.cpp \
     vcaptain.cpp \
     vopenedlistmenu.cpp \
-    vorphanfile.cpp
+    vorphanfile.cpp \
+    vcodeblockhighlighthelper.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -103,7 +104,8 @@ HEADERS  += vmainwindow.h \
     vcaptain.h \
     vopenedlistmenu.h \
     vnavigationmode.h \
-    vorphanfile.h
+    vorphanfile.h \
+    vcodeblockhighlighthelper.h
 
 RESOURCES += \
     vnote.qrc \

+ 235 - 0
src/vcodeblockhighlighthelper.cpp

@@ -0,0 +1,235 @@
+#include "vcodeblockhighlighthelper.h"
+
+#include <QDebug>
+#include <QStringList>
+#include "vdocument.h"
+#include "utils/vutils.h"
+
+VCodeBlockHighlightHelper::VCodeBlockHighlightHelper(HGMarkdownHighlighter *p_highlighter,
+                                                     VDocument *p_vdoc,
+                                                     MarkdownConverterType p_type)
+    : QObject(p_highlighter), m_highlighter(p_highlighter), m_vdocument(p_vdoc),
+      m_type(p_type), m_timeStamp(0)
+{
+    connect(m_highlighter, &HGMarkdownHighlighter::codeBlocksUpdated,
+            this, &VCodeBlockHighlightHelper::handleCodeBlocksUpdated);
+    connect(m_vdocument, &VDocument::textHighlighted,
+            this, &VCodeBlockHighlightHelper::handleTextHighlightResult);
+    connect(m_vdocument, &VDocument::readyToHighlightText,
+            m_highlighter, &HGMarkdownHighlighter::updateHighlight);
+}
+
+QString VCodeBlockHighlightHelper::unindentCodeBlock(const QString &p_text)
+{
+    if (p_text.isEmpty()) {
+        return p_text;
+    }
+
+    QStringList lines = p_text.split('\n');
+    V_ASSERT(lines[0].trimmed().startsWith("```"));
+    V_ASSERT(lines.size() > 1);
+
+    QRegExp regExp("(^\\s*)");
+    regExp.indexIn(lines[0]);
+    V_ASSERT(regExp.captureCount() == 1);
+    int nrSpaces = regExp.capturedTexts()[1].size();
+
+    if (nrSpaces == 0) {
+        return p_text;
+    }
+
+    QString res = lines[0].right(lines[0].size() - nrSpaces);
+    for (int i = 1; i < lines.size(); ++i) {
+        const QString &line = lines[i];
+
+        int idx = 0;
+        while (idx < nrSpaces && idx < line.size() && line[idx].isSpace()) {
+            ++idx;
+        }
+        res = res + "\n" + line.right(line.size() - idx);
+    }
+
+    return res;
+}
+
+void VCodeBlockHighlightHelper::handleCodeBlocksUpdated(const QList<VCodeBlock> &p_codeBlocks)
+{
+    int curStamp = m_timeStamp.fetchAndAddRelaxed(1) + 1;
+    m_codeBlocks = p_codeBlocks;
+    for (int i = 0; i < m_codeBlocks.size(); ++i) {
+        QString unindentedText = unindentCodeBlock(m_codeBlocks[i].m_text);
+        m_vdocument->highlightTextAsync(unindentedText, i, curStamp);
+    }
+}
+
+void VCodeBlockHighlightHelper::handleTextHighlightResult(const QString &p_html,
+                                                          int p_id,
+                                                          int p_timeStamp)
+{
+    int curStamp = m_timeStamp.load();
+    // Abandon obsolete result.
+    if (curStamp != p_timeStamp) {
+        return;
+    }
+    parseHighlightResult(p_timeStamp, p_id, p_html);
+}
+
+static void revertEscapedHtml(QString &p_html)
+{
+    p_html.replace("&gt;", ">").replace("&lt;", "<").replace("&amp;", "&");
+}
+
+// Search @p_tokenStr in @p_text from p_index. Spaces after `\n` will not make
+// a difference in the match. The matched range will be returned as
+// [@p_start, @p_end]. Update @p_index to @p_end + 1.
+// Set @p_start and @p_end to -1 to indicate mismatch.
+static void matchTokenRelaxed(const QString &p_text, const QString &p_tokenStr,
+                              int &p_index, int &p_start, int &p_end)
+{
+    QString regStr = QRegExp::escape(p_tokenStr);
+    // Do not replace the ending '\n'.
+    regStr.replace(QRegExp("\n(?!$)"), "\\s+");
+    QRegExp regExp(regStr);
+    p_start = p_text.indexOf(regExp, p_index);
+    if (p_start == -1) {
+        p_end = -1;
+        return;
+    }
+
+    p_end = p_start + regExp.matchedLength() - 1;
+    p_index = p_end + 1;
+}
+
+// For now, we could only handle code blocks outside the list.
+void VCodeBlockHighlightHelper::parseHighlightResult(int p_timeStamp,
+                                                     int p_idx,
+                                                     const QString &p_html)
+{
+    const VCodeBlock &block = m_codeBlocks.at(p_idx);
+    int startPos = block.m_startPos;
+    QString text = block.m_text;
+
+    QList<HLUnitPos> hlUnits;
+
+    bool failed = true;
+
+    QXmlStreamReader xml(p_html);
+
+    // Must have a fenced line at the front.
+    // textIndex is the start index in the code block text to search for.
+    int textIndex = text.indexOf('\n');
+    if (textIndex == -1) {
+        goto exit;
+    }
+    ++textIndex;
+
+    if (xml.readNextStartElement()) {
+        if (xml.name() != "pre") {
+            goto exit;
+        }
+
+        if (!xml.readNextStartElement()) {
+            goto exit;
+        }
+
+        if (xml.name() != "code") {
+            goto exit;
+        }
+
+        while (xml.readNext()) {
+            if (xml.isCharacters()) {
+                // Revert the HTML escape to match.
+                QString tokenStr = xml.text().toString();
+                revertEscapedHtml(tokenStr);
+
+                int start, end;
+                matchTokenRelaxed(text, tokenStr, textIndex, start, end);
+                if (start == -1) {
+                    failed = true;
+                    goto exit;
+                }
+            } else if (xml.isStartElement()) {
+                if (xml.name() != "span") {
+                    failed = true;
+                    goto exit;
+                }
+                if (!parseSpanElement(xml, startPos, text, textIndex, hlUnits)) {
+                    failed = true;
+                    goto exit;
+                }
+            } else if (xml.isEndElement()) {
+                if (xml.name() != "code" && xml.name() != "pre") {
+                    failed = true;
+                } else {
+                    failed = false;
+                }
+                goto exit;
+            } else {
+                failed = true;
+                goto exit;
+            }
+        }
+    }
+
+exit:
+    // Pass result back to highlighter.
+    int curStamp = m_timeStamp.load();
+    // Abandon obsolete result.
+    if (curStamp != p_timeStamp) {
+        return;
+    }
+
+    if (xml.hasError() || failed) {
+        qWarning() << "fail to parse highlighted result"
+                   << "stamp:" << p_timeStamp << "index:" << p_idx << p_html;
+        hlUnits.clear();
+    }
+
+    // We need to call this function anyway to trigger the rehighlight.
+    m_highlighter->setCodeBlockHighlights(hlUnits);
+}
+
+bool VCodeBlockHighlightHelper::parseSpanElement(QXmlStreamReader &p_xml,
+                                                 int p_startPos,
+                                                 const QString &p_text,
+                                                 int &p_index,
+                                                 QList<HLUnitPos> &p_units)
+{
+    int unitStart = p_index;
+    QString style = p_xml.attributes().value("class").toString();
+
+    while (p_xml.readNext()) {
+        if (p_xml.isCharacters()) {
+            // Revert the HTML escape to match.
+            QString tokenStr = p_xml.text().toString();
+            revertEscapedHtml(tokenStr);
+
+            int start, end;
+            matchTokenRelaxed(p_text, tokenStr, p_index, start, end);
+            if (start == -1) {
+                return false;
+            }
+        } else if (p_xml.isStartElement()) {
+            if (p_xml.name() != "span") {
+                return false;
+            }
+
+            // Sub-span.
+            if (!parseSpanElement(p_xml, p_startPos, p_text, p_index, p_units)) {
+                return false;
+            }
+        } else if (p_xml.isEndElement()) {
+            if (p_xml.name() != "span") {
+                return false;
+            }
+
+            // Got a complete span.
+            HLUnitPos unit(unitStart + p_startPos, p_index - unitStart, style);
+            p_units.append(unit);
+            return true;
+        } else {
+            return false;
+        }
+    }
+    return false;
+}

+ 49 - 0
src/vcodeblockhighlighthelper.h

@@ -0,0 +1,49 @@
+#ifndef VCODEBLOCKHIGHLIGHTHELPER_H
+#define VCODEBLOCKHIGHLIGHTHELPER_H
+
+#include <QObject>
+#include <QList>
+#include <QAtomicInteger>
+#include <QXmlStreamReader>
+#include "vconfigmanager.h"
+
+class VDocument;
+
+class VCodeBlockHighlightHelper : public QObject
+{
+    Q_OBJECT
+public:
+    VCodeBlockHighlightHelper(HGMarkdownHighlighter *p_highlighter,
+                              VDocument *p_vdoc, MarkdownConverterType p_type);
+
+signals:
+
+private slots:
+    void handleCodeBlocksUpdated(const QList<VCodeBlock> &p_codeBlocks);
+    void handleTextHighlightResult(const QString &p_html, int p_id, int p_timeStamp);
+
+private:
+    void parseHighlightResult(int p_timeStamp, int p_idx, const QString &p_html);
+
+    // @p_startPos: the global position of the start of the code block;
+    // @p_text: the raw text of the code block;
+    // @p_index: the start index of the span element within @p_text;
+    // @p_units: all the highlight units of this code block;
+    bool parseSpanElement(QXmlStreamReader &p_xml, int p_startPos,
+                          const QString &p_text, int &p_index,
+                          QList<HLUnitPos> &p_units);
+    // @p_text: text of fenced code block.
+    // Get the indent level of the first line (fence) and unindent the whole block
+    // to make the fence at the highest indent level.
+    // This operation is to make sure JS could handle the code block correctly
+    // without any context.
+    QString unindentCodeBlock(const QString &p_text);
+
+    HGMarkdownHighlighter *m_highlighter;
+    VDocument *m_vdocument;
+    MarkdownConverterType m_type;
+    QAtomicInteger<int> m_timeStamp;
+    QList<VCodeBlock> m_codeBlocks;
+};
+
+#endif // VCODEBLOCKHIGHLIGHTHELPER_H

+ 4 - 0
src/vconfigmanager.cpp

@@ -96,6 +96,9 @@ void VConfigManager::initialize()
         m_webZoomFactor = VUtils::calculateScaleFactor();
         qDebug() << "set WebZoomFactor to" << m_webZoomFactor;
     }
+
+    m_enableCodeBlockHighlight = getConfigFromSettings("global",
+                                                       "enable_code_block_highlight").toBool();
 }
 
 void VConfigManager::readPredefinedColorsFromSettings()
@@ -243,6 +246,7 @@ void VConfigManager::updateMarkdownEditStyle()
     VStyleParser parser;
     parser.parseMarkdownStyle(styleStr);
     mdHighlightingStyles = parser.fetchMarkdownStyles(baseEditFont);
+    m_codeBlockStyles = parser.fetchCodeBlockStyles(baseEditFont);
     mdEditPalette = baseEditPalette;
     mdEditFont = baseEditFont;
     QMap<QString, QMap<QString, QString>> styles;

+ 30 - 0
src/vconfigmanager.h

@@ -50,6 +50,8 @@ public:
 
     inline QVector<HighlightingStyle> getMdHighlightingStyles() const;
 
+    inline QMap<QString, QTextCharFormat> getCodeBlockStyles() const;
+
     inline QString getWelcomePagePath() const;
 
     inline QString getTemplateCssUrl() const;
@@ -135,6 +137,9 @@ public:
     inline QString getEditorCurrentLineBackground() const;
     inline QString getEditorCurrentLineVimBackground() const;
 
+    inline bool getEnableCodeBlockHighlight() const;
+    inline void setEnableCodeBlockHighlight(bool p_enabled);
+
 private:
     void updateMarkdownEditStyle();
     QVariant getConfigFromSettings(const QString &section, const QString &key);
@@ -151,6 +156,7 @@ private:
     QFont mdEditFont;
     QPalette mdEditPalette;
     QVector<HighlightingStyle> mdHighlightingStyles;
+    QMap<QString, QTextCharFormat> m_codeBlockStyles;
     QString welcomePagePath;
     QString templateCssUrl;
     int curNotebookIndex;
@@ -213,6 +219,9 @@ private:
     // Current line background color in editor in Vim mode.
     QString m_editorCurrentLineVimBackground;
 
+    // Enable colde block syntax highlight.
+    bool m_enableCodeBlockHighlight;
+
     // The name of the config file in each directory
     static const QString dirConfigFileName;
     // The name of the default configuration file
@@ -239,6 +248,11 @@ inline QVector<HighlightingStyle> VConfigManager::getMdHighlightingStyles() cons
     return mdHighlightingStyles;
 }
 
+inline QMap<QString, QTextCharFormat> VConfigManager::getCodeBlockStyles() const
+{
+    return m_codeBlockStyles;
+}
+
 inline QString VConfigManager::getWelcomePagePath() const
 {
     return welcomePagePath;
@@ -609,4 +623,20 @@ inline QString VConfigManager::getEditorCurrentLineVimBackground() const
 {
     return m_editorCurrentLineVimBackground;
 }
+
+inline bool VConfigManager::getEnableCodeBlockHighlight() const
+{
+    return m_enableCodeBlockHighlight;
+}
+
+inline void VConfigManager::setEnableCodeBlockHighlight(bool p_enabled)
+{
+    if (m_enableCodeBlockHighlight == p_enabled) {
+        return;
+    }
+    m_enableCodeBlockHighlight = p_enabled;
+    setConfigToSettings("global", "enable_code_block_highlight",
+                        m_enableCodeBlockHighlight);
+}
+
 #endif // VCONFIGMANAGER_H

+ 15 - 0
src/vdocument.cpp

@@ -59,3 +59,18 @@ void VDocument::keyPressEvent(int p_key, bool p_ctrl, bool p_shift)
 {
     emit keyPressed(p_key, p_ctrl, p_shift);
 }
+
+void VDocument::highlightTextAsync(const QString &p_text, int p_id, int p_timeStamp)
+{
+    emit requestHighlightText(p_text, p_id, p_timeStamp);
+}
+
+void VDocument::highlightTextCB(const QString &p_html, int p_id, int p_timeStamp)
+{
+    emit textHighlighted(p_html, p_id, p_timeStamp);
+}
+
+void VDocument::noticeReadyToHighlightText()
+{
+    emit readyToHighlightText();
+}

+ 8 - 0
src/vdocument.h

@@ -18,6 +18,9 @@ public:
     QString getToc();
     void scrollToAnchor(const QString &anchor);
     void setHtml(const QString &html);
+    // Request to highlight a segment text.
+    // Use p_id to identify the result.
+    void highlightTextAsync(const QString &p_text, int p_id, int p_timeStamp);
 
 public slots:
     // Will be called in the HTML side
@@ -26,6 +29,8 @@ public slots:
     void setLog(const QString &p_log);
     void keyPressEvent(int p_key, bool p_ctrl, bool p_shift);
     void updateText();
+    void highlightTextCB(const QString &p_html, int p_id, int p_timeStamp);
+    void noticeReadyToHighlightText();
 
 signals:
     void textChanged(const QString &text);
@@ -35,6 +40,9 @@ signals:
     void htmlChanged(const QString &html);
     void logChanged(const QString &p_log);
     void keyPressed(int p_key, bool p_ctrl, bool p_shift);
+    void requestHighlightText(const QString &p_text, int p_id, int p_timeStamp);
+    void textHighlighted(const QString &p_html, int p_id, int p_timeStamp);
+    void readyToHighlightText();
 
 private:
     QString m_toc;

+ 3 - 2
src/vedittab.cpp

@@ -57,7 +57,7 @@ void VEditTab::setupUI()
     switch (m_file->getDocType()) {
     case DocType::Markdown:
         if (m_file->isModifiable()) {
-            m_textEditor = new VMdEdit(m_file, this);
+            m_textEditor = new VMdEdit(m_file, &document, mdConverterType, this);
             connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::headersChanged,
                     this, &VEditTab::updateTocFromHeaders);
             connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::statusChanged,
@@ -105,7 +105,6 @@ void VEditTab::noticeStatusChanged()
 
 void VEditTab::showFileReadMode()
 {
-    qDebug() << "read" << m_file->getName();
     isEditMode = false;
     int outlineIndex = curHeader.m_outlineIndex;
     switch (m_file->getDocType()) {
@@ -298,6 +297,8 @@ void VEditTab::setupMarkdownPreview()
 
     case MarkdownConverterType::Hoedown:
         jsFile = "qrc" + VNote::c_hoedownJsFile;
+        // Use Marked to highlight code blocks.
+        extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
         break;
 
     case MarkdownConverterType::MarkdownIt:

+ 15 - 0
src/vmainwindow.cpp

@@ -367,6 +367,16 @@ void VMainWindow::initMarkdownMenu()
     markdownMenu->addAction(mathjaxAct);
 
     mathjaxAct->setChecked(vconfig.getEnableMathjax());
+
+    markdownMenu->addSeparator();
+
+    QAction *codeBlockAct = new QAction(tr("Highlight Code Blocks In Edit Mode"), this);
+    codeBlockAct->setToolTip(tr("Enable syntax highlight within code blocks in edit mode"));
+    codeBlockAct->setCheckable(true);
+    connect(codeBlockAct, &QAction::triggered,
+            this, &VMainWindow::enableCodeBlockHighlight);
+    markdownMenu->addAction(codeBlockAct);
+    codeBlockAct->setChecked(vconfig.getEnableCodeBlockHighlight());
 }
 
 void VMainWindow::initViewMenu()
@@ -1120,6 +1130,11 @@ void VMainWindow::changeAutoList(bool p_checked)
     }
 }
 
+void VMainWindow::enableCodeBlockHighlight(bool p_checked)
+{
+    vconfig.setEnableCodeBlockHighlight(p_checked);
+}
+
 void VMainWindow::shortcutHelp()
 {
     QString locale = VUtils::getLocale();

+ 1 - 0
src/vmainwindow.h

@@ -71,6 +71,7 @@ private slots:
     void handleCaptainModeChanged(bool p_enabled);
     void changeAutoIndent(bool p_checked);
     void changeAutoList(bool p_checked);
+    void enableCodeBlockHighlight(bool p_checked);
 
 protected:
     void closeEvent(QCloseEvent *event) Q_DECL_OVERRIDE;

+ 8 - 1
src/vmdedit.cpp

@@ -1,6 +1,7 @@
 #include <QtWidgets>
 #include "vmdedit.h"
 #include "hgmarkdownhighlighter.h"
+#include "vcodeblockhighlighthelper.h"
 #include "vmdeditoperations.h"
 #include "vnote.h"
 #include "vconfigmanager.h"
@@ -13,18 +14,24 @@ extern VNote *g_vnote;
 
 enum ImageProperty { ImagePath = 1 };
 
-VMdEdit::VMdEdit(VFile *p_file, QWidget *p_parent)
+VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
+                 QWidget *p_parent)
     : VEdit(p_file, p_parent), m_mdHighlighter(NULL), m_previewImage(true)
 {
     Q_ASSERT(p_file->getDocType() == DocType::Markdown);
 
     setAcceptRichText(false);
     m_mdHighlighter = new HGMarkdownHighlighter(vconfig.getMdHighlightingStyles(),
+                                                vconfig.getCodeBlockStyles(),
                                                 500, document());
     connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted,
             this, &VMdEdit::generateEditOutline);
     connect(m_mdHighlighter, &HGMarkdownHighlighter::imageBlocksUpdated,
             this, &VMdEdit::updateImageBlocks);
+
+    m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter, p_vdoc,
+                                                    p_type);
+
     m_editOps = new VMdEditOperations(this, m_file);
     connect(m_editOps, &VEditOperations::keyStateChanged,
             this, &VMdEdit::handleEditStateChanged);

+ 6 - 1
src/vmdedit.h

@@ -8,14 +8,18 @@
 #include <QClipboard>
 #include "vtoc.h"
 #include "veditoperations.h"
+#include "vconfigmanager.h"
 
 class HGMarkdownHighlighter;
+class VCodeBlockHighlightHelper;
+class VDocument;
 
 class VMdEdit : public VEdit
 {
     Q_OBJECT
 public:
-    VMdEdit(VFile *p_file, QWidget *p_parent = 0);
+    VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
+            QWidget *p_parent = 0);
     void beginEdit() Q_DECL_OVERRIDE;
     void endEdit() Q_DECL_OVERRIDE;
     void saveFile() Q_DECL_OVERRIDE;
@@ -76,6 +80,7 @@ private:
     QString selectedImage();
 
     HGMarkdownHighlighter *m_mdHighlighter;
+    VCodeBlockHighlightHelper *m_cbHighlighter;
     QVector<QString> m_insertedImages;
     QVector<QString> m_initImages;
     QVector<VHeader> m_headers;

+ 52 - 0
src/vstyleparser.cpp

@@ -130,6 +130,58 @@ QVector<HighlightingStyle> VStyleParser::fetchMarkdownStyles(const QFont &baseFo
     return styles;
 }
 
+QMap<QString, QTextCharFormat> VStyleParser::fetchCodeBlockStyles(const QFont & p_baseFont) const
+{
+    QMap<QString, QTextCharFormat> styles;
+
+    pmh_style_attribute *attrs = markdownStyles->element_styles[pmh_VERBATIM];
+
+    // First set up the base format.
+    QTextCharFormat baseFormat = QTextCharFormatFromAttrs(attrs, p_baseFont);
+
+    while (attrs) {
+        switch (attrs->type) {
+        case pmh_attr_type_other:
+        {
+            QString attrName(attrs->name);
+            QString attrValue(attrs->value->string);
+            QTextCharFormat format;
+            format.setFontFamily(baseFormat.fontFamily());
+
+            QStringList items = attrValue.split(',', QString::SkipEmptyParts);
+            for (auto const &item : items) {
+                QString val = item.trimmed().toLower();
+                if (val == "bold") {
+                    format.setFontWeight(QFont::Bold);
+                } else if (val == "italic") {
+                    format.setFontItalic(true);
+                } else if (val == "underlined") {
+                    format.setFontUnderline(true);
+                } else {
+                    // Treat it as the color RGB value string without '#'.
+                    QColor color("#" + val);
+                    if (color.isValid()) {
+                        format.setForeground(QBrush(color));
+                    }
+                }
+            }
+
+            if (format.isValid()) {
+                styles[attrName] = format;
+            }
+            break;
+        }
+
+        default:
+            // We just only handle custom attribute here.
+            break;
+        }
+        attrs = attrs->next;
+    }
+
+    return styles;
+}
+
 void VStyleParser::fetchMarkdownEditorStyles(QPalette &palette, QFont &font,
                                              QMap<QString, QMap<QString, QString>> &styles) const
 {

+ 1 - 0
src/vstyleparser.h

@@ -26,6 +26,7 @@ public:
     // @styles: [rule] -> ([attr] -> value).
     void fetchMarkdownEditorStyles(QPalette &palette, QFont &font,
                                    QMap<QString, QMap<QString, QString>> &styles) const;
+    QMap<QString, QTextCharFormat> fetchCodeBlockStyles(const QFont &p_baseFont) const;
 
 private:
     QColor QColorFromPmhAttr(pmh_attr_argb_color *attr) const;