mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-05 13:59:52 +08:00
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:
parent
f1c101b1d8
commit
44257913f7
@ -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,7 +72,7 @@ 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];
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -178,3 +178,8 @@ var updateText = function(text) {
|
||||
}
|
||||
};
|
||||
|
||||
var highlightText = function(text, id, timeStamp) {
|
||||
var html = mdit.render(text);
|
||||
content.highlightTextCB(html, id, timeStamp);
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -131,3 +131,8 @@ var updateText = function(text) {
|
||||
}
|
||||
};
|
||||
|
||||
var highlightText = function(text, id, timeStamp) {
|
||||
var html = marked(text);
|
||||
content.highlightTextCB(html, id, timeStamp);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
src/vcodeblockhighlighthelper.cpp
Normal file
235
src/vcodeblockhighlighthelper.cpp
Normal 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(">", ">").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<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
src/vcodeblockhighlighthelper.h
Normal file
49
src/vcodeblockhighlighthelper.h
Normal 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
|
@ -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;
|
||||
|
@ -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 §ion, 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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user