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.
This commit is contained in:
Le Tan 2017-04-16 10:10:21 +08:00
parent f1c101b1d8
commit 44257913f7
22 changed files with 739 additions and 37 deletions

View File

@ -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();
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();
}
}

View File

@ -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

View File

@ -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);
}

View File

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

View File

@ -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) {

View File

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

View File

@ -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

View File

@ -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

View File

@ -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 \

View File

@ -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;
}

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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();
}

View File

@ -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;

View File

@ -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:

View File

@ -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();

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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
{

View File

@ -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;