From cfcc7e5494ed77553496769320db2fab2145c0c7 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Mon, 9 Apr 2018 18:28:18 +0800 Subject: [PATCH] support in place preview and live preview of code blocks --- src/hgmarkdownhighlighter.h | 7 + src/resources/markdown-it.js | 4 +- src/resources/markdown_template.html | 2 + src/resources/markdown_template.js | 20 ++- src/resources/marked.js | 4 +- src/resources/showdown.js | 4 +- src/vcodeblockhighlighthelper.h | 14 +- src/vdocument.cpp | 5 + src/vdocument.h | 4 + src/veditor.h | 2 + src/vlivepreviewhelper.cpp | 258 ++++++++++++++++++++++----- src/vlivepreviewhelper.h | 112 +++++++++++- src/vmdeditor.cpp | 2 +- src/vmdeditor.h | 12 ++ src/vmdtab.cpp | 21 ++- src/vpreviewmanager.cpp | 137 +++++++++++--- src/vpreviewmanager.h | 80 +++++++-- src/vtextblockdata.cpp | 6 +- src/vtextblockdata.h | 4 +- 19 files changed, 594 insertions(+), 104 deletions(-) diff --git a/src/hgmarkdownhighlighter.h b/src/hgmarkdownhighlighter.h index 6555539e..3f79ee70 100644 --- a/src/hgmarkdownhighlighter.h +++ b/src/hgmarkdownhighlighter.h @@ -146,6 +146,8 @@ public: void clearPossiblePreviewBlocks(const QVector &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 } } +inline void HGMarkdownHighlighter::addPossiblePreviewBlock(int p_blockNumber) +{ + m_possiblePreviewBlocks.insert(p_blockNumber); +} + inline VTextBlockData *HGMarkdownHighlighter::currentBlockData() const { return static_cast(currentBlockUserData()); diff --git a/src/resources/markdown-it.js b/src/resources/markdown-it.js index dab748f0..7e08cf44 100644 --- a/src/resources/markdown-it.js +++ b/src/resources/markdown-it.js @@ -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); }; diff --git a/src/resources/markdown_template.html b/src/resources/markdown_template.html index 3b0c6c17..26a3b770 100644 --- a/src/resources/markdown_template.html +++ b/src/resources/markdown_template.html @@ -34,6 +34,8 @@ + + diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 71d4f1e5..934979ec 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -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) { diff --git a/src/resources/marked.js b/src/resources/marked.js index 4330d93c..0027f16e 100644 --- a/src/resources/marked.js +++ b/src/resources/marked.js @@ -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); } diff --git a/src/resources/showdown.js b/src/resources/showdown.js index 49ddb8f3..c711ea68 100644 --- a/src/resources/showdown.js +++ b/src/resources/showdown.js @@ -128,7 +128,7 @@ var highlightText = function(text, id, timeStamp) { var parser = new DOMParser(); var htmlDoc = parser.parseFromString("
" + html + "
", '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("
" + html + "
", 'text/html'); - highlightCodeBlocks(htmlDoc, false, false, false); + highlightCodeBlocks(htmlDoc, false, false, false, false, false); html = htmlDoc.getElementById('showdown-container').innerHTML; diff --git a/src/vcodeblockhighlighthelper.h b/src/vcodeblockhighlighthelper.h index 0f194c56..a5147a08 100644 --- a/src/vcodeblockhighlighthelper.h +++ b/src/vcodeblockhighlighthelper.h @@ -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 &p_codeBlocks); @@ -47,13 +54,6 @@ private: const QString &p_text, int &p_index, QVector &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 p_units); void addToHighlightCache(const QString &p_text, diff --git a/src/vdocument.cpp b/src/vdocument.cpp index 50dc50ad..ef0e3b14 100644 --- a/src/vdocument.cpp +++ b/src/vdocument.cpp @@ -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); +} diff --git a/src/vdocument.h b/src/vdocument.h index a4fbddd7..651a0805 100644 --- a/src/vdocument.h +++ b/src/vdocument.h @@ -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; diff --git a/src/veditor.h b/src/veditor.h index 5d866380..8cb3a36d 100644 --- a/src/veditor.h +++ b/src/veditor.h @@ -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; diff --git a/src/vlivepreviewhelper.cpp b/src/vlivepreviewhelper.cpp index fa75c27e..fed0d1e0 100644 --- a/src/vlivepreviewhelper.cpp +++ b/src/vlivepreviewhelper.cpp @@ -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 &p_codeBlocks) { - if (!m_livePreviewEnabled) { + if (!m_livePreviewEnabled && !m_inplacePreviewEnabled) { return; } @@ -61,29 +127,32 @@ void VLivePreviewHelper::updateCodeBlocks(const QVector &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 &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 > 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(); +} diff --git a/src/vlivepreviewhelper.h b/src/vlivepreviewhelper.h index 365eb138..d23bf015 100644 --- a/src/vlivepreviewhelper.h +++ b/src/vlivepreviewhelper.h @@ -2,14 +2,95 @@ #define VLIVEPREVIEWHELPER_H #include +#include #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 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 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 &p_codeBlocks); + void webAsyncResultReady(int p_id, const QString &p_lang, const QString &p_result); + +signals: + void inplacePreviewCodeBlockUpdated(const QVector > &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 m_codeBlocks; + QVector 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 diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index bedc34d9..fabc2cb0 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -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); diff --git a/src/vmdeditor.h b/src/vmdeditor.h index 00c92b96..09ff78d3 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -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 diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index 5bafe626..07583f3b 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -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); } diff --git a/src/vpreviewmanager.cpp b/src/vpreviewmanager.cpp index d46e25d7..3c74c617 100644 --- a/src/vpreviewmanager.cpp +++ b/src/vpreviewmanager.cpp @@ -5,6 +5,8 @@ #include #include #include +#include + #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 &p_imageRegions) +void VPreviewManager::updateImageLinks(const QVector &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(i)); clearBlockObsoletePreviewInfo(ts, static_cast(i)); - clearObsoleteImages(ts, static_cast(i)); } } -void VPreviewManager::previewImages(TS p_timeStamp) +void VPreviewManager::previewImages(TS p_timeStamp, const QVector &p_imageRegions) { QVector 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 &p_imageLinks) +void VPreviewManager::fetchImageLinksFromRegions(QVector p_imageRegions, + QVector &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 ® = m_imageRegions[i]; + for (int i = 0; i < p_imageRegions.size(); ++i) { + VElementRegion ® = p_imageRegions[i]; QTextBlock block = doc->findBlock(reg.m_startPos); if (!block.isValid()) { continue; @@ -143,7 +145,7 @@ void VPreviewManager::fetchImageLinksFromRegions(QVector &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 &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 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 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 > &p_images) +{ + QSet 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(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 &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 affectedBlocks; QVector obsoleteBlocks; - auto blocks = m_highlighter->getPossiblePreviewBlocks(); + const QSet &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 > &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); +} diff --git a/src/vpreviewmanager.h b/src/vpreviewmanager.h index 6fb47066..3e7d4808 100644 --- a/src/vpreviewmanager.h +++ b/src/vpreviewmanager.h @@ -6,6 +6,8 @@ #include #include #include +#include + #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 &p_imageRegions); + void updateImageLinks(const QVector &p_imageRegions); + + void updateCodeBlocks(const QVector > &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 &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 &p_imageLinks); + void fetchImageLinksFromRegions(QVector p_imageRegions, + QVector &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 &p_imageLinks); + // Update the preview info of related blocks according to @p_images. + void updateBlockPreviewInfo(TS p_timeStamp, + PreviewSource p_source, + const QVector > &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 &p_image); QHash &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 m_imageRegions; - // Map from URL to name in the resource manager. // Used for downloading images. QHash 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 m_imageCaches[(int)PreviewSource::MaxNumberOfSources]; @@ -150,4 +200,14 @@ inline QHash &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 diff --git a/src/vtextblockdata.cpp b/src/vtextblockdata.cpp index 87dd3987..1dcb5344 100644 --- a/src/vtextblockdata.cpp +++ b/src/vtextblockdata.cpp @@ -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 diff --git a/src/vtextblockdata.h b/src/vtextblockdata.h index 03ec0a30..7b447212 100644 --- a/src/vtextblockdata.h +++ b/src/vtextblockdata.h @@ -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;