From 44257913f7a7aed4a2f7f0c5afd5dc281daddca6 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Sun, 16 Apr 2017 10:10:21 +0800 Subject: [PATCH] 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. --- src/hgmarkdownhighlighter.cpp | 183 ++++++++++++++++++++-- src/hgmarkdownhighlighter.h | 62 +++++++- src/resources/hoedown.js | 12 ++ src/resources/markdown-it.js | 5 + src/resources/markdown_template.js | 33 ++-- src/resources/marked.js | 5 + src/resources/styles/default.mdhl | 37 ++++- src/resources/vnote.ini | 2 + src/src.pro | 6 +- src/vcodeblockhighlighthelper.cpp | 235 +++++++++++++++++++++++++++++ src/vcodeblockhighlighthelper.h | 49 ++++++ src/vconfigmanager.cpp | 4 + src/vconfigmanager.h | 30 ++++ src/vdocument.cpp | 15 ++ src/vdocument.h | 8 + src/vedittab.cpp | 5 +- src/vmainwindow.cpp | 15 ++ src/vmainwindow.h | 1 + src/vmdedit.cpp | 9 +- src/vmdedit.h | 7 +- src/vstyleparser.cpp | 52 +++++++ src/vstyleparser.h | 1 + 22 files changed, 739 insertions(+), 37 deletions(-) create mode 100644 src/vcodeblockhighlighthelper.cpp create mode 100644 src/vcodeblockhighlighthelper.h diff --git a/src/hgmarkdownhighlighter.cpp b/src/hgmarkdownhighlighter.cpp index 68409b63..f129f48c 100644 --- a/src/hgmarkdownhighlighter.cpp +++ b/src/hgmarkdownhighlighter.cpp @@ -1,6 +1,11 @@ #include #include +#include +#include #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 &styles, int waitInterval, +HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector &styles, + const QMap &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 &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 &units = blockHighlights[blockNum]; + const QVector &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 &styles) -{ - this->highlightingStyles = styles; + // Highlight CodeBlock using VCodeBlockHighlightHelper. + if (m_codeBlockHighlights.size() > blockNum) { + const QVector &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 formats; + formats.reserve(units.size()); + // formatIndex[i] is the index in @formats which is the format of the + // ith character. + QVector 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 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 &p_units) +{ + if (p_units.isEmpty()) { + goto exit; + } + + { + QVector> 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 &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(); + } +} diff --git a/src/hgmarkdownhighlighter.h b/src/hgmarkdownhighlighter.h index 0fabc88c..67f9dccb 100644 --- a/src/hgmarkdownhighlighter.h +++ b/src/hgmarkdownhighlighter.h @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include extern "C" { #include @@ -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 &styles, int waitInterval, + HGMarkdownHighlighter(const QVector &styles, + const QMap &codeBlockStyles, + int waitInterval, QTextDocument *parent = 0); ~HGMarkdownHighlighter(); - void setStyles(const QVector &styles); // Request to update highlihgt (re-parse and re-highlight) - void updateHighlight(); + void setCodeBlockHighlights(const QList &p_units); signals: void highlightCompleted(); void imageBlocksUpdated(QSet p_blocks); + void codeBlocksUpdated(const QList &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 highlightingStyles; + QMap m_codeBlockStyles; QVector > 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 > m_codeBlockHighlights; + + int m_numOfCodeBlockHighlightsToRecv; + // Block numbers containing image link(s). QSet 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 diff --git a/src/resources/hoedown.js b/src/resources/hoedown.js index 0b0da2b4..b04cc59d 100644 --- a/src/resources/hoedown.js +++ b/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); +} + diff --git a/src/resources/markdown-it.js b/src/resources/markdown-it.js index a4d3afbe..3d728356 100644 --- a/src/resources/markdown-it.js +++ b/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); +} + diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 966fd00f..4ebfaba8 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -1,20 +1,6 @@ var content; var keyState = 0; -new QWebChannel(qt.webChannelTransport, - function(channel) { - content = channel.objects.content; - if (typeof updateHtml == "function") { - updateHtml(content.html); - content.htmlChanged.connect(updateHtml); - } - if (typeof updateText == "function") { - content.textChanged.connect(updateText); - content.updateText(); - } - content.requestScrollToAnchor.connect(scrollToAnchor); - }); - var VMermaidDivClass = 'mermaid-diagram'; if (typeof VEnableMermaid == 'undefined') { VEnableMermaid = false; @@ -28,6 +14,25 @@ if (typeof VEnableMathjax == 'undefined') { VEnableMathjax = false; } +new QWebChannel(qt.webChannelTransport, + function(channel) { + content = channel.objects.content; + if (typeof updateHtml == "function") { + updateHtml(content.html); + content.htmlChanged.connect(updateHtml); + } + if (typeof updateText == "function") { + content.textChanged.connect(updateText); + content.updateText(); + } + content.requestScrollToAnchor.connect(scrollToAnchor); + + if (typeof highlightText == "function") { + content.requestHighlightText.connect(highlightText); + content.noticeReadyToHighlightText(); + } + }); + var scrollToAnchor = function(anchor) { var anc = document.getElementById(anchor); if (anc != null) { diff --git a/src/resources/marked.js b/src/resources/marked.js index ca9913a4..d43a7774 100644 --- a/src/resources/marked.js +++ b/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); +} + diff --git a/src/resources/styles/default.mdhl b/src/resources/styles/default.mdhl index 853721b4..7475f8a4 100644 --- a/src/resources/styles/default.mdhl +++ b/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 diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index 01389110..905282f8 100644 --- a/src/resources/vnote.ini +++ b/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 diff --git a/src/src.pro b/src/src.pro index cb289766..21651d65 100644 --- a/src/src.pro +++ b/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 \ diff --git a/src/vcodeblockhighlighthelper.cpp b/src/vcodeblockhighlighthelper.cpp new file mode 100644 index 00000000..130448a0 --- /dev/null +++ b/src/vcodeblockhighlighthelper.cpp @@ -0,0 +1,235 @@ +#include "vcodeblockhighlighthelper.h" + +#include +#include +#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 &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(">", ">").replace("<", "<").replace("&", "&"); +} + +// 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 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 &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; +} diff --git a/src/vcodeblockhighlighthelper.h b/src/vcodeblockhighlighthelper.h new file mode 100644 index 00000000..1ff28376 --- /dev/null +++ b/src/vcodeblockhighlighthelper.h @@ -0,0 +1,49 @@ +#ifndef VCODEBLOCKHIGHLIGHTHELPER_H +#define VCODEBLOCKHIGHLIGHTHELPER_H + +#include +#include +#include +#include +#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 &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 &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 m_timeStamp; + QList m_codeBlocks; +}; + +#endif // VCODEBLOCKHIGHLIGHTHELPER_H diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index c3fd65e2..8907dd31 100644 --- a/src/vconfigmanager.cpp +++ b/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> styles; diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 2be1ccc0..469cc97b 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -50,6 +50,8 @@ public: inline QVector getMdHighlightingStyles() const; + inline QMap 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 §ion, const QString &key); @@ -151,6 +156,7 @@ private: QFont mdEditFont; QPalette mdEditPalette; QVector mdHighlightingStyles; + QMap 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 VConfigManager::getMdHighlightingStyles() cons return mdHighlightingStyles; } +inline QMap 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 diff --git a/src/vdocument.cpp b/src/vdocument.cpp index 60c5343e..ee5f874f 100644 --- a/src/vdocument.cpp +++ b/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(); +} diff --git a/src/vdocument.h b/src/vdocument.h index d9c34dd0..0f8a0d51 100644 --- a/src/vdocument.h +++ b/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; diff --git a/src/vedittab.cpp b/src/vedittab.cpp index ede39107..c5483f3a 100644 --- a/src/vedittab.cpp +++ b/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(m_textEditor), &VMdEdit::headersChanged, this, &VEditTab::updateTocFromHeaders); connect(dynamic_cast(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 = "\n"; break; case MarkdownConverterType::MarkdownIt: diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index b7251480..57503ab9 100644 --- a/src/vmainwindow.cpp +++ b/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(); diff --git a/src/vmainwindow.h b/src/vmainwindow.h index b077a6cd..c00cb18f 100644 --- a/src/vmainwindow.h +++ b/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; diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index 12a57ddd..5b0e9045 100644 --- a/src/vmdedit.cpp +++ b/src/vmdedit.cpp @@ -1,6 +1,7 @@ #include #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); diff --git a/src/vmdedit.h b/src/vmdedit.h index b72db889..b37afec9 100644 --- a/src/vmdedit.h +++ b/src/vmdedit.h @@ -8,14 +8,18 @@ #include #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 m_insertedImages; QVector m_initImages; QVector m_headers; diff --git a/src/vstyleparser.cpp b/src/vstyleparser.cpp index 524a381d..07bf3233 100644 --- a/src/vstyleparser.cpp +++ b/src/vstyleparser.cpp @@ -130,6 +130,58 @@ QVector VStyleParser::fetchMarkdownStyles(const QFont &baseFo return styles; } +QMap VStyleParser::fetchCodeBlockStyles(const QFont & p_baseFont) const +{ + QMap 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> &styles) const { diff --git a/src/vstyleparser.h b/src/vstyleparser.h index e54e3a3a..1152c009 100644 --- a/src/vstyleparser.h +++ b/src/vstyleparser.h @@ -26,6 +26,7 @@ public: // @styles: [rule] -> ([attr] -> value). void fetchMarkdownEditorStyles(QPalette &palette, QFont &font, QMap> &styles) const; + QMap fetchCodeBlockStyles(const QFont &p_baseFont) const; private: QColor QColorFromPmhAttr(pmh_attr_argb_color *attr) const;