diff --git a/src/dialog/vsettingsdialog.cpp b/src/dialog/vsettingsdialog.cpp index fd42a787..4d2c28d3 100644 --- a/src/dialog/vsettingsdialog.cpp +++ b/src/dialog/vsettingsdialog.cpp @@ -899,10 +899,32 @@ VMarkdownTab::VMarkdownTab(QWidget *p_parent) QLabel *colorColumnLabel = new QLabel(tr("Color column:")); colorColumnLabel->setToolTip(m_colorColumnEdit->toolTip()); + // PlantUML. + m_plantUMLModeCombo = VUtils::getComboBox(); + m_plantUMLModeCombo->setToolTip(tr("Enable PlantUML support in Markdown")); + m_plantUMLModeCombo->addItem(tr("Disabled"), PlantUMLMode::DisablePlantUML); + m_plantUMLModeCombo->addItem(tr("Online Service"), PlantUMLMode::OnlinePlantUML); + m_plantUMLModeCombo->addItem(tr("Local JAR"), PlantUMLMode::LocalPlantUML); + + m_plantUMLServerEdit = new VLineEdit(); + m_plantUMLServerEdit->setToolTip(tr("Server address for online PlantUML")); + + m_plantUMLJarEdit = new VLineEdit(); + m_plantUMLJarEdit->setToolTip(tr("Location to the PlantUML JAR executable for local PlantUML")); + + m_plantUMLDotEdit = new VLineEdit(); + m_plantUMLDotEdit->setPlaceholderText(tr("Empty to detect automatically")); + m_plantUMLDotEdit->setToolTip(tr("Location to the GraphViz executable for local PlantUML " + "(empty to let PlantUML detect it automatically)")); + QFormLayout *mainLayout = new QFormLayout(); mainLayout->addRow(tr("Note open mode:"), m_openModeCombo); mainLayout->addRow(tr("Heading sequence:"), headingSequenceLayout); mainLayout->addRow(colorColumnLabel, m_colorColumnEdit); + mainLayout->addRow(tr("PlantUML:"), m_plantUMLModeCombo); + mainLayout->addRow(tr("PlantUML server:"), m_plantUMLServerEdit); + mainLayout->addRow(tr("PlantUML JAR:"), m_plantUMLJarEdit); + mainLayout->addRow(tr("Graphviz executable:"), m_plantUMLDotEdit); setLayout(mainLayout); } @@ -921,6 +943,10 @@ bool VMarkdownTab::loadConfiguration() return false; } + if (!loadPlantUML()) { + return false; + } + return true; } @@ -938,6 +964,10 @@ bool VMarkdownTab::saveConfiguration() return false; } + if (!savePlantUML()) { + return false; + } + return true; } @@ -1009,3 +1039,20 @@ bool VMarkdownTab::saveColorColumn() return true; } +bool VMarkdownTab::loadPlantUML() +{ + m_plantUMLModeCombo->setCurrentIndex(m_plantUMLModeCombo->findData(g_config->getPlantUMLMode())); + m_plantUMLServerEdit->setText(g_config->getPlantUMLServer()); + m_plantUMLJarEdit->setText(g_config->getPlantUMLJar()); + m_plantUMLDotEdit->setText(g_config->getPlantUMLDot()); + return true; +} + +bool VMarkdownTab::savePlantUML() +{ + g_config->setPlantUMLMode(m_plantUMLModeCombo->currentData().toInt()); + g_config->setPlantUMLServer(m_plantUMLServerEdit->text()); + g_config->setPlantUMLJar(m_plantUMLJarEdit->text()); + g_config->setPlantUMLDot(m_plantUMLDotEdit->text()); + return true; +} diff --git a/src/dialog/vsettingsdialog.h b/src/dialog/vsettingsdialog.h index 0fd0d925..280e95d4 100644 --- a/src/dialog/vsettingsdialog.h +++ b/src/dialog/vsettingsdialog.h @@ -157,6 +157,9 @@ private: bool loadColorColumn(); bool saveColorColumn(); + bool loadPlantUML(); + bool savePlantUML(); + // Default note open mode for markdown. QComboBox *m_openModeCombo; @@ -166,6 +169,12 @@ private: // Color column in code block. VLineEdit *m_colorColumnEdit; + + // PlantUML. + QComboBox *m_plantUMLModeCombo; + VLineEdit *m_plantUMLServerEdit; + VLineEdit *m_plantUMLJarEdit; + VLineEdit *m_plantUMLDotEdit; }; class VSettingsDialog : public QDialog diff --git a/src/resources/docs/markdown_guide_en.md b/src/resources/docs/markdown_guide_en.md index 12cce814..cd6f78c7 100644 --- a/src/resources/docs/markdown_guide_en.md +++ b/src/resources/docs/markdown_guide_en.md @@ -108,7 +108,10 @@ As VNote suggests: - `lang` is optional to specify the language of the code; ### Diagrams -VNote supports [Flowchart.js](http://flowchart.js.org/) and [Mermaid](https://mermaidjs.github.io/) to draw diagrams such as *flowchart* and *sequence diagram*. You should use `flowchart` and `mermaid` specified as the language of the fenced code block and write the definition of your diagram within it. + +> You need to enable Flowchart.js or Mermaid in the `Markdown` menu. + +VNote supports [Flowchart.js](http://flowchart.js.org/) and [Mermaid](https://mermaidjs.github.io/) to draw diagrams such as *flowchart* and *sequence diagram*. You should use `flow` or `flowchart` and `mermaid` specified as the language of the fenced code block and write the definition of your diagram within it. ```flowchart st=>start: Start:>http://www.google.com[blank] @@ -124,7 +127,22 @@ VNote supports [Flowchart.js](http://flowchart.js.org/) and [Mermaid](https://me cond(no)->sub1(right)->op1 ``` +#### UML + +> You need to enable PlantUML in the settings. Pay attention to the privacy issue if you use online PlantUML server. You may need to prepare Java runtime, PlantUML, and Graphviz if you choose local PlantUML. + +VNote supports [PlantUML](http://plantuml.com/) to draw UML diagrams. You should use `puml` specified as the language of the fenced code block and write the definition of your diagram within it. + + ```puml + @startuml + Bob -> Alice : hello + @enduml + ``` + ### Math Formulas + +> You need to enable MathJax in the `Markdown` menu. + VNote supports math formulas via [MathJax](https://www.mathjax.org/). The default math delimiters are `$$...$$` and `\[...\]` for **displayed mathematics**, and `$...$` for **inline mathematics**. Sometimes you may need to *escape* some characters via `\`. VNote also supports displayed mathematics via fenced code block with language `mathjax` specified. The benifit of using code block is you do not have to escape most characters. diff --git a/src/resources/docs/markdown_guide_zh.md b/src/resources/docs/markdown_guide_zh.md index 6cbb4dec..94fe1cb8 100644 --- a/src/resources/docs/markdown_guide_zh.md +++ b/src/resources/docs/markdown_guide_zh.md @@ -109,7 +109,10 @@ As VNote suggests: - `lang`用于指定代码块的代码语言,可选; ### 图表 -VNote支持 [Flowchart.js](http://flowchart.js.org/) 和 [Mermaid](https://mermaidjs.github.io/) 来实现诸如*流程图*和*序列图*等图表。您需要使用代码块,并标明语言为`flowchart`或`mermaid`,然后在代码块里面定义图表。 + +> 需要在`Markdown`菜单中启用Flowchart.js或Mermaid。 + +VNote支持 [Flowchart.js](http://flowchart.js.org/) 和 [Mermaid](https://mermaidjs.github.io/) 来实现诸如*流程图*和*序列图*等图表。您需要使用代码块,并标明语言为`flow`或`flowchart`或`mermaid`,然后在代码块里面定义图表。 ```flowchart st=>start: Start:>http://www.google.com[blank] @@ -125,7 +128,22 @@ VNote支持 [Flowchart.js](http://flowchart.js.org/) 和 [Mermaid](https://merma cond(no)->sub1(right)->op1 ``` +#### UML + +> 需要在设置中启用PlantUML。如果使用在线的PlantUML服务器,请注意隐私问题;如果使用本地PlantUML,可能需要安装Java运行时、PlantUML以及Graphviz。 + +VNote支持 [PlantUML](http://plantuml.com/) 来实现UML图表。您需要使用代码块,并标明语言为`puml`,然后在代码块里面定义图表。 + + ```puml + @startuml + Bob -> Alice : hello + @enduml + ``` + ### 数学公式 + +> 需要在`Markdown`菜单中启用MathJax。 + VNote通过 [MathJax](https://www.mathjax.org/) 来支持数学公式。默认的**块公式**的分隔符是`$$...$$$`和`\[...\]`,**行内公式**的分隔符是`$...$`。有时候,您需要使用`\`来*转义*某些字符。 VNote也可以使用标明语言`mathjax`的代码块来实现块公式。使用代码块的一个好处是大多数情况下无需转义字符。 diff --git a/src/resources/hoedown.js b/src/resources/hoedown.js index 9c91e0b1..7cb90cbe 100644 --- a/src/resources/hoedown.js +++ b/src/resources/hoedown.js @@ -16,12 +16,15 @@ marked.setOptions({ }); var updateHtml = function(html) { + asyncJobsCount = 0; + placeholder.innerHTML = html; insertImageCaption(); var codes = document.getElementsByTagName('code'); mermaidIdx = 0; + plantUMLIdx = 0; for (var i = 0; i < codes.length; ++i) { var code = codes[i]; if (code.parentElement.tagName.toLowerCase() == 'pre') { @@ -43,6 +46,19 @@ var updateHtml = function(html) { } } else if (VEnableMathjax && code.classList.contains('language-mathjax')) { // Mathjax code block. + continue; + } else if (VPlantUMLMode != 0 + && code.classList.contains('language-puml')) { + // PlantUML code block. + if (VPlantUMLMode == 1) { + if (renderPlantUMLOneOnline(code)) { + // replaceChild() will decrease codes.length. + --i; + } + } else { + renderPlantUMLOneLocal(code); + } + continue; } diff --git a/src/resources/markdown-it.js b/src/resources/markdown-it.js index bce86b45..14d27afe 100644 --- a/src/resources/markdown-it.js +++ b/src/resources/markdown-it.js @@ -106,6 +106,8 @@ var updateText = function(text) { text = "[TOC]\n\n" + text; } + asyncJobsCount = 0; + var needToc = mdHasTocSection(text); var html = markdownToHtml(text, needToc); placeholder.innerHTML = html; @@ -113,6 +115,7 @@ var updateText = function(text) { insertImageCaption(); renderMermaid('lang-mermaid'); renderFlowchart(['lang-flowchart', 'lang-flow']); + renderPlantUML('lang-puml'); addClassToCodeBlock(); renderCodeBlockLineNumber(); diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 88578f07..1384ddb7 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -8,6 +8,8 @@ var pendingKeys = []; var VMermaidDivClass = 'mermaid-diagram'; var VFlowchartDivClass = 'flowchart-diagram'; +var VPlantUMLDivClass = 'plantuml-diagram'; + if (typeof VEnableMermaid == 'undefined') { VEnableMermaid = false; } else if (VEnableMermaid) { @@ -32,6 +34,17 @@ if (typeof VStylesToInline == 'undefined') { VStylesToInline = ''; } +// 0 - disable PlantUML; +// 1 - Use online PlantUML processor; +// 2 - Use local PlantUML processor; +if (typeof VPlantUMLMode == 'undefined') { + VPlantUMLMode = 0; +} + +if (typeof VPlantUMLServer == 'undefined') { + VPlantUMLServer = 'http://www.plantuml.com/plantuml'; +} + // Add a caption (using alt text) under the image. var VImageCenterClass = 'img-center'; var VImageCaptionClass = 'img-caption'; @@ -133,6 +146,8 @@ new QWebChannel(qt.webChannelTransport, if (typeof htmlContent == "function") { content.requestHtmlContent.connect(htmlContent); } + + content.plantUMLResultReady.connect(handlePlantUMLResult); }); var VHighlightedAnchorClass = 'highlighted-anchor'; @@ -605,6 +620,72 @@ var renderFlowchartOne = function(code) { return true; }; +var plantUMLIdx = 0; +var plantUMLCodeClass = 'plantuml_code_'; + +// @className, the class name of the PlantUML code block, such as 'lang-puml'. +var renderPlantUML = function(className) { + if (VPlantUMLMode == 0) { + return; + } + + plantUMLIdx = 0; + + var codes = document.getElementsByTagName('code'); + for (var i = 0; i < codes.length; ++i) { + var code = codes[i]; + if (code.classList.contains(className)) { + if (VPlantUMLMode == 1) { + if (renderPlantUMLOneOnline(code)) { + // replaceChild() will decrease codes.length. + --i; + } + } else { + renderPlantUMLOneLocal(code); + } + } + } +}; + +// Render @code as PlantUML graph. +// Returns true if succeeded. +var renderPlantUMLOneOnline = function(code) { + var s = unescape(encodeURIComponent(code.textContent)); + var arr = []; + for (var i = 0; i < s.length; i++) { + arr.push(s.charCodeAt(i)); + } + + var compressor = new Zopfli.RawDeflate(arr); + var compressed = compressor.compress(); + var url = VPlantUMLServer + "/" + VPlantUMLFormat + "/" + encode64_(compressed); + + var obj = null; + if (VPlantUMLFormat == 'svg') { + var svgObj = document.createElement('object'); + svgObj.type = 'image/svg+xml'; + svgObj.data = url; + + obj = document.createElement('div'); + obj.classList.add(VPlantUMLDivClass); + obj.appendChild(svgObj); + } else { + obj = document.createElement('img'); + obj.src = url; + } + + var preNode = code.parentNode; + preNode.parentNode.replaceChild(obj, preNode); + return true; +}; + +var renderPlantUMLOneLocal = function(code) { + ++asyncJobsCount; + code.classList.add(plantUMLCodeClass + plantUMLIdx); + content.processPlantUML(plantUMLIdx, VPlantUMLFormat, code.textContent); + plantUMLIdx++; +}; + var isImageBlock = function(img) { var pn = img.parentNode; return (pn.children.length == 1) && (pn.textContent == ''); @@ -704,12 +785,20 @@ var insertImageCaption = function() { } } +var asyncJobsCount = 0; + +var finishOneAsyncJob = function() { + --asyncJobsCount; + finishLogics(); +}; + // The renderer specific code should call this function once thay have finished // markdown-specifi handle logics, such as Mermaid, MathJax. var finishLogics = function() { - content.finishLogics(); - - calculateWordCount(); + if (asyncJobsCount <= 0) { + content.finishLogics(); + calculateWordCount(); + } }; // Escape @text to Html. @@ -1170,5 +1259,26 @@ var calculateWordCount = function() { var specialCodeBlock = function(lang) { return (VEnableMathjax && lang == 'mathjax') || (VEnableMermaid && lang == 'mermaid') - || (VEnableFlowchart && (lang == 'flowchart' || lang == 'flow')); + || (VEnableFlowchart && (lang == 'flowchart' || lang == 'flow')) + || (VPlantUMLMode != 0 && lang == 'puml'); +}; + +var handlePlantUMLResult = function(id, format, result) { + var code = document.getElementsByClassName(plantUMLCodeClass + id)[0]; + if (code && result.length > 0) { + var obj = null; + if (format == 'svg') { + obj = document.createElement('div'); + obj.classList.add(VPlantUMLDivClass); + obj.innerHTML = result; + } else { + obj = document.createElement('img'); + obj.src = "data:image/" + format + ";base64, " + result; + } + + var preNode = code.parentNode; + preNode.parentNode.replaceChild(obj, preNode); + } + + finishOneAsyncJob(); }; diff --git a/src/resources/marked.js b/src/resources/marked.js index 78a583f5..dd198b6c 100644 --- a/src/resources/marked.js +++ b/src/resources/marked.js @@ -50,6 +50,8 @@ var updateText = function(text) { text = "[TOC]\n\n" + text; } + asyncJobsCount = 0; + var needToc = mdHasTocSection(text); var html = markdownToHtml(text, needToc); placeholder.innerHTML = html; @@ -57,6 +59,7 @@ var updateText = function(text) { insertImageCaption(); renderMermaid('lang-mermaid'); renderFlowchart(['lang-flowchart', 'lang-flow']); + renderPlantUML('lang-puml'); addClassToCodeBlock(); renderCodeBlockLineNumber(); diff --git a/src/resources/showdown.js b/src/resources/showdown.js index 86dd3dde..aaeace80 100644 --- a/src/resources/showdown.js +++ b/src/resources/showdown.js @@ -49,7 +49,7 @@ var mdHasTocSection = function(markdown) { return n != -1; }; -var highlightCodeBlocks = function(doc, enableMermaid, enableFlowchart, enableMathJax) { +var highlightCodeBlocks = function(doc, enableMermaid, enableFlowchart, enableMathJax, enablePlantUML) { var codes = doc.getElementsByTagName('code'); for (var i = 0; i < codes.length; ++i) { var code = codes[i]; @@ -65,6 +65,9 @@ var highlightCodeBlocks = function(doc, enableMermaid, enableFlowchart, enableMa } else if (enableMathJax && code.classList.contains('language-mathjax')) { // MathJax code block. continue; + } else if (enablePlantUML && code.classList.contains('language-puml')) { + // PlantUML code block. + continue; } if (listContainsRegex(code.classList, /language-.*/)) { @@ -79,14 +82,17 @@ var updateText = function(text) { text = "[TOC]\n\n" + text; } + asyncJobsCount = 0; + var needToc = mdHasTocSection(text); var html = markdownToHtml(text, needToc); placeholder.innerHTML = html; handleToc(needToc); insertImageCaption(); - highlightCodeBlocks(document, VEnableMermaid, VEnableFlowchart, VEnableMathjax); + highlightCodeBlocks(document, VEnableMermaid, VEnableFlowchart, VEnableMathjax, VPlantUMLMode != 0); renderMermaid('language-mermaid'); renderFlowchart(['language-flowchart', 'language-flow']); + renderPlantUML('language-puml'); addClassToCodeBlock(); renderCodeBlockLineNumber(); diff --git a/src/resources/themes/v_moonlight/v_moonlight.css b/src/resources/themes/v_moonlight/v_moonlight.css index 660d3fec..87423bb1 100644 --- a/src/resources/themes/v_moonlight/v_moonlight.css +++ b/src/resources/themes/v_moonlight/v_moonlight.css @@ -164,13 +164,22 @@ table tr th :last-child, table tr td :last-child { } div.mermaid-diagram { - overflow-y: hidden; + width: fit-content; + overflow: hidden; background: #B0BEC5; color: #6C6C6C; } div.flowchart-diagram { - overflow-y: hidden; + width: fit-content; + overflow: hidden; + background: #B0BEC5; + color: #6C6C6C; +} + +div.plantuml-diagram { + width: fit-content; + overflow: hidden; background: #B0BEC5; color: #6C6C6C; } diff --git a/src/resources/themes/v_native/v_native.css b/src/resources/themes/v_native/v_native.css index 90f86d63..2ed805a0 100644 --- a/src/resources/themes/v_native/v_native.css +++ b/src/resources/themes/v_native/v_native.css @@ -169,11 +169,18 @@ table tr th :last-child, table tr td :last-child { } div.mermaid-diagram { - overflow-y: hidden; + width: fit-content; + overflow: hidden; } div.flowchart-diagram { - overflow-y: hidden; + width: fit-content; + overflow: hidden; +} + +div.plantuml-diagram { + width: fit-content; + overflow: hidden; } .img-package { diff --git a/src/resources/themes/v_pure/v_pure.css b/src/resources/themes/v_pure/v_pure.css index 74712e77..99aea318 100644 --- a/src/resources/themes/v_pure/v_pure.css +++ b/src/resources/themes/v_pure/v_pure.css @@ -170,11 +170,18 @@ table tr th :last-child, table tr td :last-child { } div.mermaid-diagram { - overflow-y: hidden; + width: fit-content; + overflow: hidden; } div.flowchart-diagram { - overflow-y: hidden; + width: fit-content; + overflow: hidden; +} + +div.plantuml-diagram { + width: fit-content; + overflow: hidden; } .img-package { diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index d045045b..c8c40086 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -34,13 +34,21 @@ language=System ; 0 - Hoedown, 1 - Marked, 2 - Markdown-it, 3 - Showdown markdown_converter=2 +; Enable Mermaid diagram enable_mermaid=false +; Enable MathJax enable_mathjax=true -; Enable flowchart.js +; Enable Flowchart.js enable_flowchart=false +; Enable PlantUML +; 0 - disable PlantUML +; 1 - online PlantUML +; 2 - local PlantUML +plantuml_mode=0 + ; -1 - calculate the factor web_zoom_factor=-1 @@ -263,6 +271,15 @@ copy_targets="Without Background"$s:b(mark):c:i:x,Evernote$e:p:b(mark|pre):c(pre enable_flash_anchor=true +; PlantUML server to convert UML script online +plantuml_server=http://www.plantuml.com/plantuml + +; PlantUML JAR location +plantuml_jar= + +; PlantUML Graphviz Dot location +plantuml_dot= + [shortcuts] ; Define shortcuts here, with each item in the form "operation=keysequence". ; Leave keysequence empty to disable the shortcut of an operation. diff --git a/src/src.pro b/src/src.pro index c6cc137b..9c22dafe 100644 --- a/src/src.pro +++ b/src/src.pro @@ -125,7 +125,8 @@ SOURCES += main.cpp\ voutlineue.cpp \ vhelpue.cpp \ vlistfolderue.cpp \ - dialog/vfixnotebookdialog.cpp + dialog/vfixnotebookdialog.cpp \ + vplantumlhelper.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -241,7 +242,8 @@ HEADERS += vmainwindow.h \ voutlineue.h \ vhelpue.h \ vlistfolderue.h \ - dialog/vfixnotebookdialog.h + dialog/vfixnotebookdialog.h \ + vplantumlhelper.h RESOURCES += \ vnote.qrc \ diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 96564d45..d8bceb49 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -618,11 +618,12 @@ QString VUtils::generateHtmlTemplate(MarkdownConverterType p_conType, g_config->getCodeBlockCssStyleUrl(p_renderCodeBlockStyle), p_isPDF); - return generateHtmlTemplate(templ, p_conType, p_wkhtmltopdf, p_addToc); + return generateHtmlTemplate(templ, p_conType, p_isPDF, p_wkhtmltopdf, p_addToc); } QString VUtils::generateHtmlTemplate(const QString &p_template, MarkdownConverterType p_conType, + bool p_isPDF, bool p_wkhtmltopdf, bool p_addToc) { @@ -721,6 +722,20 @@ QString VUtils::generateHtmlTemplate(const QString &p_template, } } + int plantUMLMode = g_config->getPlantUMLMode(); + if (plantUMLMode != PlantUMLMode::DisablePlantUML) { + if (plantUMLMode == PlantUMLMode::OnlinePlantUML) { + extraFile += "\n" + + "\n" + + "\n"; + } + + extraFile += QString("\n").arg(plantUMLMode); + + QString format = p_isPDF ? "png" : "svg"; + extraFile += QString("\n").arg(format); + } + if (g_config->getEnableImageCaption()) { extraFile += "\n"; } diff --git a/src/utils/vutils.h b/src/utils/vutils.h index cfba9c5a..b43e388c 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -368,6 +368,7 @@ private: static QString generateHtmlTemplate(const QString &p_template, MarkdownConverterType p_conType, + bool p_isPDF = false, bool p_wkhtmltopdf = false, bool p_addToc = false); diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index 45ff5822..5e2298bb 100644 --- a/src/vconfigmanager.cpp +++ b/src/vconfigmanager.cpp @@ -284,6 +284,11 @@ void VConfigManager::initialize() m_enableFlashAnchor = getConfigFromSettings("web", "enable_flash_anchor").toBool(); + + m_plantUMLMode = getConfigFromSettings("global", "plantuml_mode").toInt(); + m_plantUMLServer = getConfigFromSettings("web", "plantuml_server").toString(); + m_plantUMLJar = getConfigFromSettings("web", "plantuml_jar").toString(); + m_plantUMLDot = getConfigFromSettings("web", "plantuml_dot").toString(); } void VConfigManager::initSettings() diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index e609986f..ff8ca64c 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -210,6 +210,9 @@ public: bool getEnableMathjax() const; void setEnableMathjax(bool p_enabled); + int getPlantUMLMode() const; + void setPlantUMLMode(int p_mode); + qreal getWebZoomFactor() const; void setWebZoomFactor(qreal p_factor); bool isCustomWebZoomFactor(); @@ -460,6 +463,15 @@ public: QStringList getSearchOptions() const; void setSearchOptions(const QStringList &p_opts); + const QString &getPlantUMLServer() const; + void setPlantUMLServer(const QString &p_server); + + const QString &getPlantUMLJar() const; + void setPlantUMLJar(const QString &p_jarPath); + + const QString &getPlantUMLDot() const; + void setPlantUMLDot(const QString &p_dotPath); + private: // Look up a config from user and default settings. QVariant getConfigFromSettings(const QString §ion, const QString &key) const; @@ -852,6 +864,15 @@ private: // Whether flash anchor in read mode. bool m_enableFlashAnchor; + // PlantUML mode. + int m_plantUMLMode; + + QString m_plantUMLServer; + + QString m_plantUMLJar; + + QString m_plantUMLDot; + // The name of the config file in each directory, obsolete. // Use c_dirConfigFile instead. static const QString c_obsoleteDirConfigFile; @@ -1325,10 +1346,26 @@ inline void VConfigManager::setEnableMathjax(bool p_enabled) if (m_enableMathjax == p_enabled) { return; } + m_enableMathjax = p_enabled; setConfigToSettings("global", "enable_mathjax", m_enableMathjax); } +inline int VConfigManager::getPlantUMLMode() const +{ + return m_plantUMLMode; +} + +inline void VConfigManager::setPlantUMLMode(int p_mode) +{ + if (m_plantUMLMode == p_mode) { + return; + } + + m_plantUMLMode = p_mode; + setConfigToSettings("global", "plantuml_mode", p_mode); +} + inline qreal VConfigManager::getWebZoomFactor() const { return m_webZoomFactor; @@ -2149,4 +2186,49 @@ inline void VConfigManager::setSearchOptions(const QStringList &p_opts) { setConfigToSettings("global", "search_options", p_opts); } + +inline const QString &VConfigManager::getPlantUMLServer() const +{ + return m_plantUMLServer; +} + +inline void VConfigManager::setPlantUMLServer(const QString &p_server) +{ + if (m_plantUMLServer == p_server) { + return; + } + + m_plantUMLServer = p_server; + setConfigToSettings("web", "plantuml_server", p_server); +} + +inline const QString &VConfigManager::getPlantUMLJar() const +{ + return m_plantUMLJar; +} + +inline void VConfigManager::setPlantUMLJar(const QString &p_jarPath) +{ + if (m_plantUMLJar == p_jarPath) { + return; + } + + m_plantUMLJar = p_jarPath; + setConfigToSettings("web", "plantuml_jar", p_jarPath); +} + +inline const QString &VConfigManager::getPlantUMLDot() const +{ + return m_plantUMLDot; +} + +inline void VConfigManager::setPlantUMLDot(const QString &p_dotPath) +{ + if (m_plantUMLDot == p_dotPath) { + return; + } + + m_plantUMLDot = p_dotPath; + setConfigToSettings("web", "plantuml_dot", p_dotPath); +} #endif // VCONFIGMANAGER_H diff --git a/src/vconstants.h b/src/vconstants.h index 80759e38..4e463e4f 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -152,6 +152,13 @@ enum MarkdownConverterType Showdown }; +enum PlantUMLMode +{ + DisablePlantUML = 0, + OnlinePlantUML = 1, + LocalPlantUML = 2 +}; + struct MarkdownitOption { diff --git a/src/vdocument.cpp b/src/vdocument.cpp index 30ea6634..0c6c4fe9 100644 --- a/src/vdocument.cpp +++ b/src/vdocument.cpp @@ -1,11 +1,15 @@ #include "vdocument.h" -#include "vfile.h" + #include +#include "vfile.h" +#include "vplantumlhelper.h" + VDocument::VDocument(const VFile *v_file, QObject *p_parent) : QObject(p_parent), m_file(v_file), - m_readyToHighlight(false) + m_readyToHighlight(false), + m_plantUMLHelper(NULL) { } @@ -132,3 +136,16 @@ void VDocument::updateWordCountInfo(int p_wordCount, emit wordCountInfoUpdated(); } + +void VDocument::processPlantUML(int p_id, const QString &p_format, const QString &p_text) +{ + if (!m_plantUMLHelper) { + m_plantUMLHelper = new VPlantUMLHelper(this); + connect(m_plantUMLHelper, &VPlantUMLHelper::resultReady, + this, [this](int p_id, const QString &p_format, const QString &p_result) { + emit plantUMLResultReady(p_id, p_format, p_result); + }); + } + + m_plantUMLHelper->processAsync(p_id, p_format, p_text); +} diff --git a/src/vdocument.h b/src/vdocument.h index 8f341513..097b11db 100644 --- a/src/vdocument.h +++ b/src/vdocument.h @@ -7,6 +7,7 @@ #include "vwordcountinfo.h" class VFile; +class VPlantUMLHelper; class VDocument : public QObject { @@ -82,6 +83,9 @@ public slots: int p_charWithoutSpacesCount, int p_charWithSpacesCount); + // Web-side call this to process PlantUML locally. + void processPlantUML(int p_id, const QString &p_format, const QString &p_text); + signals: void textChanged(const QString &text); @@ -118,6 +122,8 @@ signals: void wordCountInfoUpdated(); + void plantUMLResultReady(int p_id, const QString &p_format, const QString &p_result); + private: QString m_toc; QString m_header; @@ -137,6 +143,8 @@ private: bool m_readyToTextToHtml; VWordCountInfo m_wordCountInfo; + + VPlantUMLHelper *m_plantUMLHelper; }; inline bool VDocument::isReadyToHighlight() const diff --git a/src/vhelpue.cpp b/src/vhelpue.cpp index d0de9aaa..afd61e79 100644 --- a/src/vhelpue.cpp +++ b/src/vhelpue.cpp @@ -67,7 +67,7 @@ bool VHelpUE::initListWidget() m_listWidget->addItem(tr("Ctrl+D: Cancel the command")); m_listWidget->addItem(tr("Ctrl+J: Go to next item")); m_listWidget->addItem(tr("Ctrl+K: Go to previous item")); - m_listWidget->addItem(tr("Ctrl+R: Go to current item's parent item")); + m_listWidget->addItem(tr("Ctrl+L: Go to current item's parent item")); m_listWidget->addItem(tr("Ctrl+T: Expand/Collapse current item")); m_listWidget->addItem(tr("Ctrl+S: Sort items")); m_listWidget->addItem(tr("Enter: Activate current item")); diff --git a/src/vnote.cpp b/src/vnote.cpp index 7117919f..fcd351a1 100644 --- a/src/vnote.cpp +++ b/src/vnote.cpp @@ -50,6 +50,9 @@ const QString VNote::c_mermaidForestCssFile = ":/utils/mermaid/mermaid.forest.cs const QString VNote::c_flowchartJsFile = ":/utils/flowchart.js/flowchart.min.js"; const QString VNote::c_raphaelJsFile = ":/utils/flowchart.js/raphael.min.js"; +const QString VNote::c_plantUMLJsFile = "http://s.plantuml.com/synchro2.js"; +const QString VNote::c_plantUMLZopfliJsFile = "http://s.plantuml.com/zopfli.raw.min.js"; + const QString VNote::c_highlightjsLineNumberExtraFile = ":/utils/highlightjs/highlightjs-line-numbers.min.js"; const QString VNote::c_docFileFolder = ":/resources/docs"; @@ -128,6 +131,7 @@ QString VNote::generateHtmlTemplate(const QString &p_renderBg, "code { word-break: break-all !important; }\n" "div.flowchart-diagram { overflow: hidden !important; }\n" "div.mermaid-diagram { overflow: hidden !important; }\n" + "div.plantuml-diagram { overflow: hidden !important; }\n" "a { word-break: break-all !important; }\n" "td.hljs-ln-code { white-space: pre-wrap !important; " "word-break: break-all !important; }\n"; diff --git a/src/vnote.h b/src/vnote.h index a5b6dc61..3d131eff 100644 --- a/src/vnote.h +++ b/src/vnote.h @@ -64,6 +64,10 @@ public: static const QString c_flowchartJsFile; static const QString c_raphaelJsFile; + // PlantUML + static const QString c_plantUMLJsFile; + static const QString c_plantUMLZopfliJsFile; + // Highlight.js line number plugin static const QString c_highlightjsLineNumberExtraFile; diff --git a/src/vplantumlhelper.cpp b/src/vplantumlhelper.cpp new file mode 100644 index 00000000..f942cabd --- /dev/null +++ b/src/vplantumlhelper.cpp @@ -0,0 +1,90 @@ +#include "vplantumlhelper.h" + +#include +#include + +#include "vconfigmanager.h" + +extern VConfigManager *g_config; + +#define TaskIdProperty "PlantUMLTaskId" +#define TaskFormatProperty "PlantUMLTaskFormat" + +VPlantUMLHelper::VPlantUMLHelper(QObject *p_parent) + : QObject(p_parent) +{ + prepareCommand(m_program, m_args); +} + +void VPlantUMLHelper::processAsync(int p_id, const QString &p_format, const QString &p_text) +{ + QProcess *process = new QProcess(this); + process->setProperty(TaskIdProperty, p_id); + process->setProperty(TaskFormatProperty, p_format); + connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), + this, SLOT(handleProcessFinished(int, QProcess::ExitStatus))); + + QStringList args(m_args); + args << ("-t" + p_format); + + qDebug() << m_program << args; + + process->start(m_program, args); + process->write(p_text.toUtf8()); + process->closeWriteChannel(); +} + +void VPlantUMLHelper::prepareCommand(QString &p_program, QStringList &p_args) const +{ + p_program = "java"; + + p_args << "-jar" << g_config->getPlantUMLJar(); + p_args << "-charset" << "UTF-8"; + + int nbthread = QThread::idealThreadCount(); + p_args << "-nbthread" << QString::number(nbthread > 0 ? nbthread : 1); + + const QString &dot = g_config->getPlantUMLDot(); + if (!dot.isEmpty()) { + p_args << "-graphvizdot"; + p_args << dot; + } + + p_args << "-pipe"; +} + +void VPlantUMLHelper::handleProcessFinished(int p_exitCode, QProcess::ExitStatus p_exitStatus) +{ + QProcess *process = static_cast(sender()); + int id = process->property(TaskIdProperty).toInt(); + QString format = process->property(TaskFormatProperty).toString(); + qDebug() << "process finished" << id << format << p_exitCode << p_exitStatus; + bool failed = true; + if (p_exitStatus == QProcess::NormalExit) { + if (p_exitCode < 0) { + qWarning() << "PlantUML fail" << p_exitCode; + } else { + failed = false; + QByteArray outBa = process->readAllStandardOutput(); + if (format == "svg") { + emit resultReady(id, format, QString::fromLocal8Bit(outBa)); + } else { + emit resultReady(id, format, QString::fromLocal8Bit(outBa.toBase64())); + } + } + } else { + qWarning() << "fail to start PlantUML process" << p_exitCode << p_exitStatus; + } + + if (failed) { + QByteArray errBa = process->readAllStandardError(); + if (!errBa.isEmpty()) { + QString errStr(QString::fromLocal8Bit(errBa)); + qWarning() << "PlantUML stderr:" << errStr; + } + + emit resultReady(id, format, ""); + } + + process->deleteLater(); +} diff --git a/src/vplantumlhelper.h b/src/vplantumlhelper.h new file mode 100644 index 00000000..fa733c5f --- /dev/null +++ b/src/vplantumlhelper.h @@ -0,0 +1,30 @@ +#ifndef VPLANTUMLHELPER_H +#define VPLANTUMLHELPER_H + +#include + +#include +#include + +class VPlantUMLHelper : public QObject +{ + Q_OBJECT +public: + explicit VPlantUMLHelper(QObject *p_parent = nullptr); + + void processAsync(int p_id, const QString &p_format, const QString &p_text); + + void prepareCommand(QString &p_cmd, QStringList &p_args) const; + +signals: + void resultReady(int p_id, const QString &p_format, const QString &p_result); + +private slots: + void handleProcessFinished(int p_exitCode, QProcess::ExitStatus p_exitStatus); + +private: + QString m_program; + QStringList m_args; +}; + +#endif // VPLANTUMLHELPER_H diff --git a/src/vpreviewpage.cpp b/src/vpreviewpage.cpp index d87859d7..ec5a4fb3 100644 --- a/src/vpreviewpage.cpp +++ b/src/vpreviewpage.cpp @@ -16,14 +16,14 @@ bool VPreviewPage::acceptNavigationRequest(const QUrl &p_url, bool p_isMainFrame) { Q_UNUSED(p_type); - Q_UNUSED(p_isMainFrame); if (p_url.isLocalFile()) { QString filePath = p_url.toLocalFile(); if (g_mainWin->tryOpenInternalFile(filePath)) { - qDebug() << "internal notes jump" << filePath; return false; } + } else if (!p_isMainFrame) { + return true; } QDesktopServices::openUrl(p_url); diff --git a/src/vuniversalentry.cpp b/src/vuniversalentry.cpp index 6073b2bb..4ba34503 100644 --- a/src/vuniversalentry.cpp +++ b/src/vuniversalentry.cpp @@ -352,9 +352,9 @@ void VUniversalEntry::keyPressEvent(QKeyEvent *p_event) break; - case Qt::Key_R: + case Qt::Key_L: if (VUtils::isControlModifierForVim(modifiers)) { - // Ctrl+R to go up a level. + // Ctrl+L to go up a level. if (m_lastEntry) { m_lastEntry->m_entry->selectParentItem(m_lastEntry->m_id); }