support in place preview and live preview of code blocks

This commit is contained in:
Le Tan 2018-04-09 18:28:18 +08:00
parent 0b9cc6e5b3
commit cfcc7e5494
19 changed files with 594 additions and 104 deletions

View File

@ -146,6 +146,8 @@ public:
void clearPossiblePreviewBlocks(const QVector<int> &p_blocksToClear);
void addPossiblePreviewBlock(int p_blockNumber);
// Parse and only update the highlight results for rehighlight().
void updateHighlightFast();
@ -329,6 +331,11 @@ inline void HGMarkdownHighlighter::clearPossiblePreviewBlocks(const QVector<int>
}
}
inline void HGMarkdownHighlighter::addPossiblePreviewBlock(int p_blockNumber)
{
m_possiblePreviewBlocks.insert(p_blockNumber);
}
inline VTextBlockData *HGMarkdownHighlighter::currentBlockData() const
{
return static_cast<VTextBlockData *>(currentBlockUserData());

View File

@ -43,7 +43,7 @@ var mdit = window.markdownit({
typographer: false,
langPrefix: 'lang-',
highlight: function(str, lang) {
if (lang && !specialCodeBlock(lang)) {
if (lang && (!specialCodeBlock(lang) || highlightSpecialBlocks)) {
if (hljs.getLanguage(lang)) {
return hljs.highlight(lang, str, true).value;
} else {
@ -134,7 +134,9 @@ var updateText = function(text) {
};
var highlightText = function(text, id, timeStamp) {
highlightSpecialBlocks = true;
var html = mdit.render(text);
highlightSpecialBlocks = false;
content.highlightTextCB(html, id, timeStamp);
};

View File

@ -34,6 +34,8 @@
<div id="preview-div" style="display:none;"></div>
<div id="inplace-preview-div" style="display:none;"></div>
<div id="text-html-div" style="display:none;"></div>
</body>
</html>

View File

@ -4,6 +4,8 @@ var contentDiv = document.getElementById('content-div');
var previewDiv = document.getElementById('preview-div');
var inplacePreviewDiv = document.getElementById('inplace-preview-div');
var textHtmlDiv = document.getElementById('text-html-div');
var content;
@ -85,6 +87,9 @@ if (typeof VAddTOC == 'undefined') {
VAddTOC = false;
}
// Whether highlight special blocks like puml, flowchart.
var highlightSpecialBlocks = false;
var getUrlScheme = function(url) {
var idx = url.indexOf(':');
if (idx > -1) {
@ -1204,6 +1209,7 @@ var initStylesToInline = function() {
};
// Embed the CSS styles of @ele and all its children.
// StylesToInline need to be init before.
var embedInlineStyles = function(ele) {
var tagName = ele.tagName.toLowerCase();
var props = StylesToInline.get(tagName);
@ -1373,7 +1379,7 @@ var setPreviewEnabled = function(enabled) {
};
var previewCodeBlock = function(id, lang, text, isLivePreview) {
var div = previewDiv;
var div = isLivePreview ? previewDiv : inplacePreviewDiv;
div.innerHTML = '';
div.className = '';
@ -1381,7 +1387,7 @@ var previewCodeBlock = function(id, lang, text, isLivePreview) {
|| (lang != 'flow'
&& lang != 'flowchart'
&& lang != 'mermaid'
&& (lang != 'puml' || VPlantUMLMode != 1))) {
&& (lang != 'puml' || VPlantUMLMode != 1 || !isLivePreview))) {
return;
}
@ -1399,6 +1405,16 @@ var previewCodeBlock = function(id, lang, text, isLivePreview) {
} else if (lang == 'puml') {
renderPlantUMLOneOnline(code);
}
if (!isLivePreview) {
var children = div.children;
if (children.length > 0) {
content.previewCodeBlockCB(id, lang, children[0].innerHTML);
}
div.innerHTML = '';
div.className = '';
}
};
var setPreviewContent = function(lang, html) {

View File

@ -16,7 +16,7 @@ renderer.heading = function(text, level) {
// Highlight.js to highlight code block
marked.setOptions({
highlight: function(code, lang) {
if (lang && !specialCodeBlock(lang)) {
if (lang && (!specialCodeBlock(lang) || highlightSpecialBlocks)) {
if (hljs.getLanguage(lang)) {
return hljs.highlight(lang, code, true).value;
} else {
@ -78,7 +78,9 @@ var updateText = function(text) {
};
var highlightText = function(text, id, timeStamp) {
highlightSpecialBlocks = true;
var html = marked(text);
highlightSpecialBlocks = false;
content.highlightTextCB(html, id, timeStamp);
}

View File

@ -128,7 +128,7 @@ var highlightText = function(text, id, timeStamp) {
var parser = new DOMParser();
var htmlDoc = parser.parseFromString("<div id=\"showdown-container\">" + html + "</div>", 'text/html');
highlightCodeBlocks(htmlDoc, false, false, false);
highlightCodeBlocks(htmlDoc, false, false, false, false, false);
html = htmlDoc.getElementById('showdown-container').innerHTML;
@ -142,7 +142,7 @@ var textToHtml = function(text) {
var parser = new DOMParser();
var htmlDoc = parser.parseFromString("<div id=\"showdown-container\">" + html + "</div>", 'text/html');
highlightCodeBlocks(htmlDoc, false, false, false);
highlightCodeBlocks(htmlDoc, false, false, false, false, false);
html = htmlDoc.getElementById('showdown-container').innerHTML;

View File

@ -17,6 +17,13 @@ public:
VCodeBlockHighlightHelper(HGMarkdownHighlighter *p_highlighter,
VDocument *p_vdoc, MarkdownConverterType p_type);
// @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.
static QString unindentCodeBlock(const QString &p_text);
private slots:
void handleCodeBlocksUpdated(const QVector<VCodeBlock> &p_codeBlocks);
@ -47,13 +54,6 @@ private:
const QString &p_text, int &p_index,
QVector<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);
void updateHighlightResults(int p_startPos, QVector<HLUnitPos> p_units);
void addToHighlightCache(const QString &p_text,

View File

@ -178,3 +178,8 @@ void VDocument::setPreviewContent(const QString &p_lang, const QString &p_html)
{
emit requestSetPreviewContent(p_lang, p_html);
}
void VDocument::previewCodeBlockCB(int p_id, const QString &p_lang, const QString &p_html)
{
emit codeBlockPreviewReady(p_id, p_lang, p_html);
}

View File

@ -104,6 +104,8 @@ public slots:
// Web-side call this to process Graphviz locally.
void processGraphviz(int p_id, const QString &p_format, const QString &p_text);
void previewCodeBlockCB(int p_id, const QString &p_lang, const QString &p_html);
signals:
void textChanged(const QString &text);
@ -153,6 +155,8 @@ signals:
void requestSetPreviewContent(const QString &p_lang, const QString &p_html);
void codeBlockPreviewReady(int p_id, const QString &p_lang, const QString &p_html);
private:
QString m_toc;
QString m_header;

View File

@ -159,6 +159,8 @@ public:
virtual QTextDocument *documentW() const = 0;
virtual int tabStopWidthW() const = 0;
virtual void setTabStopWidthW(int p_width) = 0;
virtual QTextCursor textCursorW() const = 0;

View File

@ -7,6 +7,7 @@
#include "vconfigmanager.h"
#include "vgraphvizhelper.h"
#include "vplantumlhelper.h"
#include "vcodeblockhighlighthelper.h"
extern VConfigManager *g_config;
@ -22,24 +23,88 @@ extern VConfigManager *g_config;
#define INDEX_MASK 0x00ffffffUL
CodeBlockPreviewInfo::CodeBlockPreviewInfo()
{
}
CodeBlockPreviewInfo::CodeBlockPreviewInfo(const VCodeBlock &p_cb)
: m_codeBlock(p_cb)
{
}
void CodeBlockPreviewInfo::clearImageData()
{
m_imgData.clear();
m_inplacePreview.clear();
}
void CodeBlockPreviewInfo::updateNonContent(const QTextDocument *p_doc,
const VCodeBlock &p_cb)
{
m_codeBlock.updateNonContent(p_cb);
if (m_inplacePreview.isNull()) {
return;
}
QTextBlock block = p_doc->findBlockByNumber(m_codeBlock.m_endBlock);
if (block.isValid()) {
m_inplacePreview->m_startPos = block.position();
m_inplacePreview->m_endPos = block.position() + block.length();
m_inplacePreview->m_blockPos = block.position();
m_inplacePreview->m_blockNumber = m_codeBlock.m_endBlock;
} else {
m_inplacePreview->clear();
}
}
// Update inplace preview according to m_imgData.
void CodeBlockPreviewInfo::updateInplacePreview(const VEditor *p_editor,
const QTextDocument *p_doc)
{
QTextBlock block = p_doc->findBlockByNumber(m_codeBlock.m_endBlock);
if (block.isValid()) {
if (m_inplacePreview.isNull()) {
m_inplacePreview.reset(new VImageToPreview());
}
// m_image will be generated when signaling out.
m_inplacePreview->m_startPos = block.position();
m_inplacePreview->m_endPos = block.position() + block.length();
m_inplacePreview->m_blockPos = block.position();
m_inplacePreview->m_blockNumber = m_codeBlock.m_endBlock;
m_inplacePreview->m_padding = VPreviewManager::calculateBlockMargin(block,
p_editor->tabStopWidthW());
m_inplacePreview->m_name = QString::number(getImageIndex());
m_inplacePreview->m_isBlock = true;
} else {
m_inplacePreview->clear();
}
}
VLivePreviewHelper::VLivePreviewHelper(VEditor *p_editor,
VDocument *p_document,
QObject *p_parent)
: QObject(p_parent),
m_editor(p_editor),
m_document(p_document),
m_doc(p_editor->documentW()),
m_cbIndex(-1),
m_livePreviewEnabled(false),
m_inplacePreviewEnabled(false),
m_graphvizHelper(NULL),
m_plantUMLHelper(NULL)
{
connect(m_editor->object(), &VEditorObject::cursorPositionChanged,
this, &VLivePreviewHelper::handleCursorPositionChanged);
connect(m_document, &VDocument::codeBlockPreviewReady,
this, &VLivePreviewHelper::webAsyncResultReady);
m_flowchartEnabled = g_config->getEnableFlowchart();
m_mermaidEnabled = g_config->getEnableMermaid();
m_plantUMLMode = g_config->getPlantUMLMode();
m_graphvizEnabled = g_config->getEnableGraphviz();
m_mathjaxEnabled = g_config->getEnableMathjax();
}
bool VLivePreviewHelper::isPreviewLang(const QString &p_lang) const
@ -47,12 +112,13 @@ bool VLivePreviewHelper::isPreviewLang(const QString &p_lang) const
return (m_flowchartEnabled && (p_lang == "flow" || p_lang == "flowchart"))
|| (m_mermaidEnabled && p_lang == "mermaid")
|| (m_plantUMLMode != PlantUMLMode::DisablePlantUML && p_lang == "puml")
|| (m_graphvizEnabled && p_lang == "dot");
|| (m_graphvizEnabled && p_lang == "dot")
|| (m_mathjaxEnabled && p_lang == "mathjax");
}
void VLivePreviewHelper::updateCodeBlocks(const QVector<VCodeBlock> &p_codeBlocks)
{
if (!m_livePreviewEnabled) {
if (!m_livePreviewEnabled && !m_inplacePreviewEnabled) {
return;
}
@ -61,29 +127,32 @@ void VLivePreviewHelper::updateCodeBlocks(const QVector<VCodeBlock> &p_codeBlock
int cursorBlock = m_editor->textCursorW().block().blockNumber();
int idx = 0;
bool needUpdate = true;
int nrCached = 0;
for (auto const & cb : p_codeBlocks) {
if (!isPreviewLang(cb.m_lang)) {
for (auto const & vcb : p_codeBlocks) {
if (!isPreviewLang(vcb.m_lang)) {
continue;
}
bool cached = false;
if (idx < m_codeBlocks.size()) {
CodeBlock &vcb = m_codeBlocks[idx];
if (vcb.m_codeBlock.equalContent(cb)) {
vcb.m_codeBlock.updateNonContent(cb);
CodeBlockPreviewInfo &cb = m_codeBlocks[idx];
if (cb.codeBlock().equalContent(vcb)) {
cb.updateNonContent(m_doc, vcb);
cached = true;
++nrCached;
} else {
vcb.m_codeBlock = cb;
vcb.m_cachedResult.clear();
cb.setCodeBlock(vcb);
}
} else {
m_codeBlocks.append(CodeBlock());
m_codeBlocks[idx].m_codeBlock = cb;
m_codeBlocks.append(CodeBlockPreviewInfo(vcb));
}
if (cb.m_startBlock <= cursorBlock && cb.m_endBlock >= cursorBlock) {
if (m_inplacePreviewEnabled
&& !m_codeBlocks[idx].inplacePreviewReady()) {
processForInplacePreview(idx);
}
if (m_livePreviewEnabled
&& vcb.m_startBlock <= cursorBlock
&& vcb.m_endBlock >= cursorBlock) {
if (lastIndex == idx && cached) {
needUpdate = false;
}
@ -96,9 +165,7 @@ void VLivePreviewHelper::updateCodeBlocks(const QVector<VCodeBlock> &p_codeBlock
m_codeBlocks.resize(idx);
qDebug() << "VLivePreviewHelper cache" << nrCached << "code blocks of" << m_codeBlocks.size();
if (needUpdate) {
if (m_livePreviewEnabled && needUpdate) {
updateLivePreview();
}
}
@ -115,11 +182,11 @@ void VLivePreviewHelper::handleCursorPositionChanged()
int mid = left;
while (left <= right) {
mid = (left + right) / 2;
const CodeBlock &cb = m_codeBlocks[mid];
if (cb.m_codeBlock.m_startBlock <= cursorBlock && cb.m_codeBlock.m_endBlock >= cursorBlock) {
const CodeBlockPreviewInfo &cb = m_codeBlocks[mid];
const VCodeBlock &vcb = cb.codeBlock();
if (vcb.m_startBlock <= cursorBlock && vcb.m_endBlock >= cursorBlock) {
break;
} else if (cb.m_codeBlock.m_startBlock > cursorBlock) {
} else if (vcb.m_startBlock > cursorBlock) {
right = mid - 1;
} else {
left = mid + 1;
@ -136,9 +203,10 @@ void VLivePreviewHelper::handleCursorPositionChanged()
static QString removeFence(const QString &p_text)
{
Q_ASSERT(p_text.startsWith("```") && p_text.endsWith("```"));
int idx = p_text.indexOf('\n') + 1;
return p_text.mid(idx, p_text.size() - idx - 3);
QString text = VCodeBlockHighlightHelper::unindentCodeBlock(p_text);
Q_ASSERT(text.startsWith("```") && text.endsWith("```"));
int idx = text.indexOf('\n') + 1;
return text.mid(idx, text.size() - idx - 3);
}
void VLivePreviewHelper::updateLivePreview()
@ -148,43 +216,41 @@ void VLivePreviewHelper::updateLivePreview()
}
Q_ASSERT(!(m_cbIndex & ~INDEX_MASK));
const CodeBlock &cb = m_codeBlocks[m_cbIndex];
QString text = removeFence(cb.m_codeBlock.m_text);
qDebug() << "updateLivePreview" << m_cbIndex << cb.m_codeBlock.m_lang;
if (cb.m_codeBlock.m_lang == "dot") {
const CodeBlockPreviewInfo &cb = m_codeBlocks[m_cbIndex];
const VCodeBlock &vcb = cb.codeBlock();
if (vcb.m_lang == "dot") {
if (!m_graphvizHelper) {
m_graphvizHelper = new VGraphvizHelper(this);
connect(m_graphvizHelper, &VGraphvizHelper::resultReady,
this, &VLivePreviewHelper::localAsyncResultReady);
}
if (cb.m_cachedResult.isEmpty()) {
if (!cb.hasImageData()) {
m_graphvizHelper->processAsync(m_cbIndex | LANG_PREFIX_GRAPHVIZ | TYPE_LIVE_PREVIEW,
"svg",
text);
removeFence(vcb.m_text));
} else {
qDebug() << "use cached preview result of code block" << m_cbIndex;
m_document->setPreviewContent(cb.m_codeBlock.m_lang, cb.m_cachedResult);
m_document->setPreviewContent(vcb.m_lang, cb.imageData());
}
} else if (cb.m_codeBlock.m_lang == "puml" && m_plantUMLMode == PlantUMLMode::LocalPlantUML) {
} else if (vcb.m_lang == "puml" && m_plantUMLMode == PlantUMLMode::LocalPlantUML) {
if (!m_plantUMLHelper) {
m_plantUMLHelper = new VPlantUMLHelper(this);
connect(m_plantUMLHelper, &VPlantUMLHelper::resultReady,
this, &VLivePreviewHelper::localAsyncResultReady);
}
if (cb.m_cachedResult.isEmpty()) {
if (!cb.hasImageData()) {
m_plantUMLHelper->processAsync(m_cbIndex | LANG_PREFIX_PLANTUML | TYPE_LIVE_PREVIEW,
"svg",
text);
removeFence(vcb.m_text));
} else {
qDebug() << "use cached preview result of code block" << m_cbIndex;
m_document->setPreviewContent(cb.m_codeBlock.m_lang, cb.m_cachedResult);
m_document->setPreviewContent(vcb.m_lang, cb.imageData());
}
} else {
m_document->previewCodeBlock(m_cbIndex, cb.m_codeBlock.m_lang, text, true);
} else if (vcb.m_lang != "puml") {
m_document->previewCodeBlock(m_cbIndex,
vcb.m_lang,
removeFence(vcb.m_text),
true);
}
}
@ -202,6 +268,20 @@ void VLivePreviewHelper::setLivePreviewEnabled(bool p_enabled)
}
}
void VLivePreviewHelper::setInplacePreviewEnabled(bool p_enabled)
{
if (m_inplacePreviewEnabled == p_enabled) {
return;
}
m_inplacePreviewEnabled = p_enabled;
if (!m_livePreviewEnabled) {
for (auto & cb : m_codeBlocks) {
cb.clearImageData();
}
}
}
void VLivePreviewHelper::localAsyncResultReady(int p_id,
const QString &p_format,
const QString &p_result)
@ -211,6 +291,7 @@ void VLivePreviewHelper::localAsyncResultReady(int p_id,
int idx = p_id & INDEX_MASK;
bool livePreview = (p_id & TYPE_MASK) == TYPE_LIVE_PREVIEW;
QString lang;
switch (p_id & LANG_PREFIX_MASK) {
case LANG_PREFIX_PLANTUML:
lang = "puml";
@ -224,12 +305,105 @@ void VLivePreviewHelper::localAsyncResultReady(int p_id,
return;
}
if (idx >= m_codeBlocks.size()) {
return;
}
CodeBlockPreviewInfo &cb = m_codeBlocks[idx];
cb.setImageData(p_format, p_result);
if (livePreview) {
if (idx != m_cbIndex) {
return;
}
m_codeBlocks[idx].m_cachedResult = p_result;
m_document->setPreviewContent(lang, p_result);
} else {
// Inplace preview.
cb.updateInplacePreview(m_editor, m_doc);
updateInplacePreview();
}
}
void VLivePreviewHelper::processForInplacePreview(int p_idx)
{
CodeBlockPreviewInfo &cb = m_codeBlocks[p_idx];
const VCodeBlock &vcb = cb.codeBlock();
if (vcb.m_lang == "dot") {
if (!m_graphvizHelper) {
m_graphvizHelper = new VGraphvizHelper(this);
connect(m_graphvizHelper, &VGraphvizHelper::resultReady,
this, &VLivePreviewHelper::localAsyncResultReady);
}
if (cb.hasImageData()) {
cb.updateInplacePreview(m_editor, m_doc);
updateInplacePreview();
} else {
m_graphvizHelper->processAsync(p_idx | LANG_PREFIX_GRAPHVIZ | TYPE_INPLACE_PREVIEW,
"svg",
removeFence(vcb.m_text));
}
} else if (vcb.m_lang == "puml" && m_plantUMLMode == PlantUMLMode::LocalPlantUML) {
if (!m_plantUMLHelper) {
m_plantUMLHelper = new VPlantUMLHelper(this);
connect(m_plantUMLHelper, &VPlantUMLHelper::resultReady,
this, &VLivePreviewHelper::localAsyncResultReady);
}
if (cb.hasImageData()) {
cb.updateInplacePreview(m_editor, m_doc);
updateInplacePreview();
} else {
m_plantUMLHelper->processAsync(p_idx | LANG_PREFIX_PLANTUML | TYPE_INPLACE_PREVIEW,
"svg",
removeFence(vcb.m_text));
}
} else if (vcb.m_lang == "flow"
|| vcb.m_lang == "flowchart") {
m_document->previewCodeBlock(p_idx,
vcb.m_lang,
removeFence(vcb.m_text),
false);
}
}
void VLivePreviewHelper::updateInplacePreview()
{
QVector<QSharedPointer<VImageToPreview> > images;
for (int i = 0; i < m_codeBlocks.size(); ++i) {
CodeBlockPreviewInfo &cb = m_codeBlocks[i];
if (cb.inplacePreviewReady() && cb.hasImageData()) {
Q_ASSERT(!cb.inplacePreview().isNull());
// Generate the image.
cb.inplacePreview()->m_image.loadFromData(cb.imageData().toUtf8(),
cb.imageFormat().toLocal8Bit().data());
images.append(cb.inplacePreview());
}
}
emit inplacePreviewCodeBlockUpdated(images);
// Clear image.
for (int i = 0; i < m_codeBlocks.size(); ++i) {
CodeBlockPreviewInfo &cb = m_codeBlocks[i];
if (cb.inplacePreviewReady() && cb.hasImageData()) {
cb.inplacePreview()->m_image = QPixmap();
}
}
}
void VLivePreviewHelper::webAsyncResultReady(int p_id,
const QString &p_lang,
const QString &p_result)
{
Q_UNUSED(p_lang);
if (p_id >= m_codeBlocks.size() || p_result.isEmpty()) {
return;
}
CodeBlockPreviewInfo &cb = m_codeBlocks[p_id];
cb.setImageData(QStringLiteral("svg"), p_result);
cb.updateInplacePreview(m_editor, m_doc);
updateInplacePreview();
}

View File

@ -2,14 +2,95 @@
#define VLIVEPREVIEWHELPER_H
#include <QObject>
#include <QTextDocument>
#include "hgmarkdownhighlighter.h"
#include "vpreviewmanager.h"
class VEditor;
class VDocument;
class VGraphvizHelper;
class VPlantUMLHelper;
class CodeBlockPreviewInfo
{
public:
CodeBlockPreviewInfo();
explicit CodeBlockPreviewInfo(const VCodeBlock &p_cb);
void clearImageData();
void updateNonContent(const QTextDocument *p_doc, const VCodeBlock &p_cb);
void updateInplacePreview(const VEditor *p_editor, const QTextDocument *p_doc);
VCodeBlock &codeBlock()
{
return m_codeBlock;
}
const VCodeBlock &codeBlock() const
{
return m_codeBlock;
}
void setCodeBlock(const VCodeBlock &p_cb)
{
m_codeBlock = p_cb;
clearImageData();
}
bool inplacePreviewReady() const
{
return !m_inplacePreview.isNull();
}
bool hasImageData() const
{
return !m_imgData.isEmpty();
}
const QString &imageData() const
{
return m_imgData;
}
const QString &imageFormat() const
{
return m_imgFormat;
}
void setImageData(const QString &p_format, const QString &p_data)
{
m_imgFormat = p_format;
m_imgData = p_data;
}
const QSharedPointer<VImageToPreview> inplacePreview() const
{
return m_inplacePreview;
}
private:
static int getImageIndex()
{
static int index = 0;
return ++index;
}
VCodeBlock m_codeBlock;
QString m_imgData;
QString m_imgFormat;
QSharedPointer<VImageToPreview> m_inplacePreview;
};
// Manage live preview and inplace preview.
class VLivePreviewHelper : public QObject
{
Q_OBJECT
@ -22,30 +103,42 @@ public:
void setLivePreviewEnabled(bool p_enabled);
void setInplacePreviewEnabled(bool p_enabled);
bool isPreviewEnabled() const;
public slots:
void updateCodeBlocks(const QVector<VCodeBlock> &p_codeBlocks);
void webAsyncResultReady(int p_id, const QString &p_lang, const QString &p_result);
signals:
void inplacePreviewCodeBlockUpdated(const QVector<QSharedPointer<VImageToPreview> > &p_images);
private slots:
void handleCursorPositionChanged();
void localAsyncResultReady(int p_id, const QString &p_format, const QString &p_result);
private:
bool isPreviewLang(const QString &p_lang) const;
struct CodeBlock
{
VCodeBlock m_codeBlock;
QString m_cachedResult;
};
// Get image data for this code block for inplace preview.
void processForInplacePreview(int p_idx);
// Emit signal to update inplace preview.
void updateInplacePreview();
// Sorted by m_startBlock in ascending order.
QVector<CodeBlock> m_codeBlocks;
QVector<CodeBlockPreviewInfo> m_codeBlocks;
VEditor *m_editor;
VDocument *m_document;
QTextDocument *m_doc;
// Current previewed code block index in m_codeBlocks.
int m_cbIndex;
@ -53,11 +146,18 @@ private:
bool m_mermaidEnabled;
int m_plantUMLMode;
bool m_graphvizEnabled;
bool m_mathjaxEnabled;
bool m_livePreviewEnabled;
bool m_inplacePreviewEnabled;
VGraphvizHelper *m_graphvizHelper;
VPlantUMLHelper *m_plantUMLHelper;
};
inline bool VLivePreviewHelper::isPreviewEnabled() const
{
return m_inplacePreviewEnabled || m_livePreviewEnabled;
}
#endif // VLIVEPREVIEWHELPER_H

View File

@ -85,7 +85,7 @@ VMdEditor::VMdEditor(VFile *p_file,
m_previewMgr = new VPreviewManager(this, m_mdHighlighter);
connect(m_mdHighlighter, &HGMarkdownHighlighter::imageLinksUpdated,
m_previewMgr, &VPreviewManager::imageLinksUpdated);
m_previewMgr, &VPreviewManager::updateImageLinks);
connect(m_previewMgr, &VPreviewManager::requestUpdateImageLinks,
m_mdHighlighter, &HGMarkdownHighlighter::updateHighlight);

View File

@ -74,6 +74,8 @@ public:
HGMarkdownHighlighter *getMarkdownHighlighter() const;
VPreviewManager *getPreviewManager() const;
public slots:
bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE;
@ -91,6 +93,11 @@ public:
return document();
}
int tabStopWidthW() const Q_DECL_OVERRIDE
{
return tabStopWidth();
}
void setTabStopWidthW(int p_width) Q_DECL_OVERRIDE
{
setTabStopWidth(p_width);
@ -273,4 +280,9 @@ inline HGMarkdownHighlighter *VMdEditor::getMarkdownHighlighter() const
{
return m_mdHighlighter;
}
inline VPreviewManager *VMdEditor::getPreviewManager() const
{
return m_previewMgr;
}
#endif // VMDEDITOR_H

View File

@ -515,6 +515,15 @@ void VMdTab::setupMarkdownEditor()
enableHeadingSequence(m_enableHeadingSequence);
m_editor->reloadFile();
m_splitter->insertWidget(0, m_editor);
m_livePreviewHelper = new VLivePreviewHelper(m_editor, m_document, this);
connect(m_editor->getMarkdownHighlighter(), &HGMarkdownHighlighter::codeBlocksUpdated,
m_livePreviewHelper, &VLivePreviewHelper::updateCodeBlocks);
connect(m_editor->getPreviewManager(), &VPreviewManager::previewEnabledChanged,
m_livePreviewHelper, &VLivePreviewHelper::setInplacePreviewEnabled);
connect(m_livePreviewHelper, &VLivePreviewHelper::inplacePreviewCodeBlockUpdated,
m_editor->getPreviewManager(), &VPreviewManager::updateCodeBlocks);
m_livePreviewHelper->setInplacePreviewEnabled(m_editor->getPreviewManager()->isPreviewEnabled());
}
void VMdTab::updateOutlineFromHtml(const QString &p_tocHtml)
@ -1019,6 +1028,13 @@ void VMdTab::tabIsReady(TabReady p_mode)
}
});
}
if (m_editor
&& p_mode == TabReady::ReadMode
&& m_livePreviewHelper->isPreviewEnabled()) {
// Need to re-preview.
m_editor->getMarkdownHighlighter()->updateHighlight();
}
}
void VMdTab::writeBackupFile()
@ -1375,11 +1391,6 @@ void VMdTab::setCurrentMode(Mode p_mode)
newSizes.append(a);
newSizes.append(b);
m_splitter->setSizes(newSizes);
Q_ASSERT(!m_livePreviewHelper);
m_livePreviewHelper = new VLivePreviewHelper(m_editor, m_document, this);
connect(m_editor->getMarkdownHighlighter(), &HGMarkdownHighlighter::codeBlocksUpdated,
m_livePreviewHelper, &VLivePreviewHelper::updateCodeBlocks);
} else if (factor != m_previewWebViewState->m_zoomFactor) {
m_webViewer->setZoomFactor(m_previewWebViewState->m_zoomFactor);
}

View File

@ -5,6 +5,8 @@
#include <QDir>
#include <QUrl>
#include <QVector>
#include <QTextLayout>
#include "vconfigmanager.h"
#include "utils/vutils.h"
#include "vdownloader.h"
@ -17,24 +19,25 @@ VPreviewManager::VPreviewManager(VMdEditor *p_editor, HGMarkdownHighlighter *p_h
m_editor(p_editor),
m_document(p_editor->document()),
m_highlighter(p_highlighter),
m_previewEnabled(false),
m_timeStamp(0)
m_previewEnabled(false)
{
for (int i = 0; i < (int)PreviewSource::MaxNumberOfSources; ++i) {
m_timeStamps[i] = 0;
}
m_downloader = new VDownloader(this);
connect(m_downloader, &VDownloader::downloadFinished,
this, &VPreviewManager::imageDownloaded);
}
void VPreviewManager::imageLinksUpdated(const QVector<VElementRegion> &p_imageRegions)
void VPreviewManager::updateImageLinks(const QVector<VElementRegion> &p_imageRegions)
{
if (!m_previewEnabled) {
return;
}
TS ts = ++m_timeStamp;
m_imageRegions = p_imageRegions;
previewImages(ts);
TS ts = ++timeStamp(PreviewSource::ImageLink);
previewImages(ts, p_imageRegions);
}
void VPreviewManager::imageDownloaded(const QByteArray &p_data, const QString &p_url)
@ -70,6 +73,8 @@ void VPreviewManager::setPreviewEnabled(bool p_enabled)
if (m_previewEnabled != p_enabled) {
m_previewEnabled = p_enabled;
emit previewEnabledChanged(p_enabled);
if (!m_previewEnabled) {
clearPreview();
} else {
@ -80,21 +85,17 @@ void VPreviewManager::setPreviewEnabled(bool p_enabled)
void VPreviewManager::clearPreview()
{
m_imageRegions.clear();
long long ts = ++m_timeStamp;
for (int i = 0; i < (int)PreviewSource::MaxNumberOfSources; ++i) {
TS ts = ++timeStamp(static_cast<PreviewSource>(i));
clearBlockObsoletePreviewInfo(ts, static_cast<PreviewSource>(i));
clearObsoleteImages(ts, static_cast<PreviewSource>(i));
}
}
void VPreviewManager::previewImages(TS p_timeStamp)
void VPreviewManager::previewImages(TS p_timeStamp, const QVector<VElementRegion> &p_imageRegions)
{
QVector<ImageLinkInfo> imageLinks;
fetchImageLinksFromRegions(imageLinks);
fetchImageLinksFromRegions(p_imageRegions, imageLinks);
updateBlockPreviewInfo(p_timeStamp, imageLinks);
@ -116,20 +117,21 @@ static bool isAllSpaces(const QString &p_text, int p_start, int p_end)
return true;
}
void VPreviewManager::fetchImageLinksFromRegions(QVector<ImageLinkInfo> &p_imageLinks)
void VPreviewManager::fetchImageLinksFromRegions(QVector<VElementRegion> p_imageRegions,
QVector<ImageLinkInfo> &p_imageLinks)
{
p_imageLinks.clear();
if (m_imageRegions.isEmpty()) {
if (p_imageRegions.isEmpty()) {
return;
}
p_imageLinks.reserve(m_imageRegions.size());
p_imageLinks.reserve(p_imageRegions.size());
QTextDocument *doc = m_editor->document();
for (int i = 0; i < m_imageRegions.size(); ++i) {
VElementRegion &reg = m_imageRegions[i];
for (int i = 0; i < p_imageRegions.size(); ++i) {
VElementRegion &reg = p_imageRegions[i];
QTextBlock block = doc->findBlock(reg.m_startPos);
if (!block.isValid()) {
continue;
@ -143,7 +145,7 @@ void VPreviewManager::fetchImageLinksFromRegions(QVector<ImageLinkInfo> &p_image
reg.m_endPos,
blockStart,
block.blockNumber(),
calculateBlockMargin(block));
calculateBlockMargin(block, m_editor->tabStopWidthW()));
if ((reg.m_startPos == blockStart
|| isAllSpaces(text, 0, reg.m_startPos - blockStart))
&& (reg.m_endPos == blockEnd
@ -256,7 +258,23 @@ QString VPreviewManager::imageResourceName(const ImageLinkInfo &p_link)
return name;
}
int VPreviewManager::calculateBlockMargin(const QTextBlock &p_block)
QString VPreviewManager::imageResourceNameFromCodeBlock(const QSharedPointer<VImageToPreview> &p_image)
{
QString name = "CODE_BLOCK_" + p_image->m_name;
if (m_editor->containsImage(name)) {
return name;
}
// Add it to the resource.
if (p_image->m_image.isNull()) {
return QString();
}
m_editor->addImage(name, p_image->m_image);
return name;
}
int VPreviewManager::calculateBlockMargin(const QTextBlock &p_block, int p_tabStopWidth)
{
static QHash<QString, int> spaceWidthOfFonts;
@ -272,7 +290,7 @@ int VPreviewManager::calculateBlockMargin(const QTextBlock &p_block)
} else if (text[i] == ' ') {
++nrSpaces;
} else if (text[i] == '\t') {
nrSpaces += m_editor->tabStopWidth();
nrSpaces += p_tabStopWidth;
}
}
@ -281,7 +299,14 @@ int VPreviewManager::calculateBlockMargin(const QTextBlock &p_block)
}
int spaceWidth = 0;
QFont font = p_block.charFormat().font();
QFont font;
QVector<QTextLayout::FormatRange> fmts = p_block.layout()->formats();
if (fmts.isEmpty()) {
font = p_block.charFormat().font();
} else {
font = fmts.first().format.font();
}
QString fontName = font.toString();
auto it = spaceWidthOfFonts.find(fontName);
if (it != spaceWidthOfFonts.end()) {
@ -327,11 +352,57 @@ void VPreviewManager::updateBlockPreviewInfo(TS p_timeStamp,
<< imageCache(PreviewSource::ImageLink).size()
<< blockData->toString();
}
// TODO: may need to call m_editor->update()?
}
void VPreviewManager::updateBlockPreviewInfo(TS p_timeStamp,
PreviewSource p_source,
const QVector<QSharedPointer<VImageToPreview> > &p_images)
{
QSet<int> affectedBlocks;
for (auto const & img : p_images) {
if (img.isNull()) {
continue;
}
QTextBlock block = m_document->findBlockByNumber(img->m_blockNumber);
if (!block.isValid()) {
continue;
}
QString name = imageResourceNameFromCodeBlock(img);
if (name.isEmpty()) {
continue;
}
VTextBlockData *blockData = dynamic_cast<VTextBlockData *>(block.userData());
Q_ASSERT(blockData);
VPreviewInfo *info = new VPreviewInfo(p_source,
p_timeStamp,
img->m_startPos - img->m_blockPos,
img->m_endPos - img->m_blockPos,
img->m_padding,
!img->m_isBlock,
name,
m_editor->imageSize(name));
bool tsUpdated = blockData->insertPreviewInfo(info);
imageCache(p_source).insert(name, p_timeStamp);
if (!tsUpdated) {
// No need to relayout the block if only timestamp is updated.
affectedBlocks.insert(img->m_blockNumber);
m_highlighter->addPossiblePreviewBlock(img->m_blockNumber);
}
}
// Relayout these blocks since they may not have been changed.
m_editor->relayout(affectedBlocks);
m_editor->update();
}
void VPreviewManager::clearObsoleteImages(long long p_timeStamp, PreviewSource p_source)
{
auto cache = imageCache(p_source);
QHash<QString, long long> &cache = imageCache(p_source);
for (auto it = cache.begin(); it != cache.end();) {
if (it.value() < p_timeStamp) {
@ -348,7 +419,7 @@ void VPreviewManager::clearBlockObsoletePreviewInfo(long long p_timeStamp,
{
QSet<int> affectedBlocks;
QVector<int> obsoleteBlocks;
auto blocks = m_highlighter->getPossiblePreviewBlocks();
const QSet<int> &blocks = m_highlighter->getPossiblePreviewBlocks();
qDebug() << "possible preview blocks" << blocks;
for (auto i : blocks) {
QTextBlock block = m_document->findBlockByNumber(i);
@ -384,5 +455,21 @@ void VPreviewManager::refreshPreview()
clearPreview();
// No need to request updating code blocks since this will also update them.
requestUpdateImageLinks();
}
void VPreviewManager::updateCodeBlocks(const QVector<QSharedPointer<VImageToPreview> > &p_images)
{
if (!m_previewEnabled) {
return;
}
TS ts = ++timeStamp(PreviewSource::CodeBlock);
updateBlockPreviewInfo(ts, PreviewSource::CodeBlock, p_images);
clearBlockObsoletePreviewInfo(ts, PreviewSource::CodeBlock);
clearObsoleteImages(ts, PreviewSource::CodeBlock);
}

View File

@ -6,6 +6,8 @@
#include <QTextBlock>
#include <QHash>
#include <QVector>
#include <QSharedPointer>
#include "hgmarkdownhighlighter.h"
#include "vmdeditor.h"
#include "vtextblockdata.h"
@ -14,7 +16,41 @@ class VDownloader;
typedef long long TS;
// Info about image to preview.
struct VImageToPreview
{
void clear()
{
m_startPos = m_endPos = m_blockPos = m_blockNumber = -1;
m_padding = 0;
m_image = QPixmap();
m_name.clear();
m_isBlock = true;
};
int m_startPos;
int m_endPos;
// Position of this block.
int m_blockPos;
int m_blockNumber;
// Left padding of this block in pixels.
int m_padding;
QPixmap m_image;
// If @m_name are the same, then they are the same imges.
QString m_name;
// Whether it is an image block.
bool m_isBlock;
};
// Manage inplace preview.
class VPreviewManager : public QObject
{
Q_OBJECT
@ -29,14 +65,23 @@ public:
// Refresh all the preview.
void refreshPreview();
bool isPreviewEnabled() const;
// Calculate the block margin (prefix spaces) in pixels.
static int calculateBlockMargin(const QTextBlock &p_block, int p_tabStopWidth);
public slots:
// Image links were updated from the highlighter.
void imageLinksUpdated(const QVector<VElementRegion> &p_imageRegions);
void updateImageLinks(const QVector<VElementRegion> &p_imageRegions);
void updateCodeBlocks(const QVector<QSharedPointer<VImageToPreview> > &p_images);
signals:
// Request highlighter to update image links.
void requestUpdateImageLinks();
void previewEnabledChanged(bool p_enabled);
private slots:
// Non-local image downloaded for preview.
void imageDownloaded(const QByteArray &p_data, const QString &p_url);
@ -92,11 +137,12 @@ private:
};
// Start to preview images according to image links.
void previewImages(TS p_timeStamp);
void previewImages(TS p_timeStamp, const QVector<VElementRegion> &p_imageRegions);
// According to m_imageRegions, fetch the image link Url.
// According to p_imageRegions, fetch the image link Url.
// @p_imageRegions: output.
void fetchImageLinksFromRegions(QVector<ImageLinkInfo> &p_imageLinks);
void fetchImageLinksFromRegions(QVector<VElementRegion> p_imageRegions,
QVector<ImageLinkInfo> &p_imageLinks);
// Fetch the image link's URL if there is only one link.
QString fetchImageUrlToPreview(const QString &p_text);
@ -108,13 +154,17 @@ private:
// Update the preview info of related blocks according to @p_imageLinks.
void updateBlockPreviewInfo(TS p_timeStamp, const QVector<ImageLinkInfo> &p_imageLinks);
// Update the preview info of related blocks according to @p_images.
void updateBlockPreviewInfo(TS p_timeStamp,
PreviewSource p_source,
const QVector<QSharedPointer<VImageToPreview> > &p_images);
// Get the name of the image in the resource manager.
// Will add the image to the resource manager if not exists.
// Returns empty if fail to add the image to the resource manager.
QString imageResourceName(const ImageLinkInfo &p_link);
// Calculate the block margin (prefix spaces) in pixels.
int calculateBlockMargin(const QTextBlock &p_block);
QString imageResourceNameFromCodeBlock(const QSharedPointer<VImageToPreview> &p_image);
QHash<QString, long long> &imageCache(PreviewSource p_source);
@ -122,6 +172,8 @@ private:
void clearBlockObsoletePreviewInfo(long long p_timeStamp, PreviewSource p_source);
TS &timeStamp(PreviewSource p_source);
VMdEditor *m_editor;
QTextDocument *m_document;
@ -133,14 +185,12 @@ private:
// Whether preview is enabled.
bool m_previewEnabled;
// Regions of all the image links.
QVector<VElementRegion> m_imageRegions;
// Map from URL to name in the resource manager.
// Used for downloading images.
QHash<QString, QString> m_urlToName;
TS m_timeStamp;
// Timestamp per each preview source.
TS m_timeStamps[(int)PreviewSource::MaxNumberOfSources];
// Used to discard obsolete images. One per each preview source.
QHash<QString, long long> m_imageCaches[(int)PreviewSource::MaxNumberOfSources];
@ -150,4 +200,14 @@ inline QHash<QString, long long> &VPreviewManager::imageCache(PreviewSource p_so
{
return m_imageCaches[(int)p_source];
}
inline TS &VPreviewManager::timeStamp(PreviewSource p_source)
{
return m_timeStamps[(int)p_source];
}
inline bool VPreviewManager::isPreviewEnabled() const
{
return m_previewEnabled;
}
#endif // VPREVIEWMANAGER_H

View File

@ -17,8 +17,9 @@ VTextBlockData::~VTextBlockData()
m_previews.clear();
}
void VTextBlockData::insertPreviewInfo(VPreviewInfo *p_info)
bool VTextBlockData::insertPreviewInfo(VPreviewInfo *p_info)
{
bool tsUpdated = false;
bool inserted = false;
for (auto it = m_previews.begin(); it != m_previews.end();) {
VPreviewInfo *ele = *it;
@ -33,6 +34,7 @@ void VTextBlockData::insertPreviewInfo(VPreviewInfo *p_info)
delete ele;
*it = p_info;
inserted = true;
tsUpdated = true;
qDebug() << "update eixsting image's timestamp" << p_info->m_imageInfo.toString();
break;
} else if (p_info->m_imageInfo.intersect(ele->m_imageInfo)) {
@ -53,6 +55,8 @@ void VTextBlockData::insertPreviewInfo(VPreviewInfo *p_info)
}
Q_ASSERT(checkOrder());
return tsUpdated;
}
QString VTextBlockData::toString() const

View File

@ -8,6 +8,7 @@
enum class PreviewSource
{
ImageLink = 0,
CodeBlock,
MaxNumberOfSources
};
@ -137,7 +138,8 @@ public:
~VTextBlockData();
// Insert @p_info into m_previews, preserving the order.
void insertPreviewInfo(VPreviewInfo *p_info);
// Returns true if only timestamp is updated.
bool insertPreviewInfo(VPreviewInfo *p_info);
// For degub only.
QString toString() const;