Przeglądaj źródła

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 lat temu
rodzic
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;