Kaynağa Gözat

support local PlantUml and Graphviz (#1776)

Le Tan 4 yıl önce
ebeveyn
işleme
79abddd802
36 değiştirilmiş dosya ile 1168 ekleme ve 46 silme
  1. 1 1
      libs/vtextedit
  2. 1 1
      src/core/configmgr.cpp
  3. 3 1
      src/core/configmgr.h
  4. 1 0
      src/core/core.pri
  5. 3 0
      src/core/fileopenparameters.h
  6. 45 0
      src/core/markdowneditorconfig.cpp
  7. 20 0
      src/core/markdowneditorconfig.h
  8. 18 0
      src/core/noncopyable.h
  9. 2 4
      src/core/vnotex.h
  10. 7 0
      src/data/core/vnotex.json
  11. 3 0
      src/data/extra/themes/moonlight/text-editor.theme
  12. 3 2
      src/data/extra/web/js/graphrenderer.js
  13. 71 5
      src/data/extra/web/js/graphviz.js
  14. 4 0
      src/data/extra/web/js/markdownviewer.js
  15. 43 13
      src/data/extra/web/js/plantuml.js
  16. 16 0
      src/data/extra/web/js/vnotex.js
  17. 5 0
      src/data/extra/web/js/vxworker.js
  18. 148 3
      src/widgets/dialogs/settings/markdowneditorpage.cpp
  19. 10 0
      src/widgets/dialogs/settings/markdowneditorpage.h
  20. 201 0
      src/widgets/editors/graphhelper.cpp
  21. 91 0
      src/widgets/editors/graphhelper.h
  22. 74 0
      src/widgets/editors/graphvizhelper.cpp
  23. 36 0
      src/widgets/editors/graphvizhelper.h
  24. 8 0
      src/widgets/editors/markdowneditor.cpp
  25. 2 0
      src/widgets/editors/markdowneditor.h
  26. 34 0
      src/widgets/editors/markdownvieweradapter.cpp
  27. 12 0
      src/widgets/editors/markdownvieweradapter.h
  28. 104 0
      src/widgets/editors/plantumlhelper.cpp
  29. 41 0
      src/widgets/editors/plantumlhelper.h
  30. 95 10
      src/widgets/editors/previewhelper.cpp
  31. 16 0
      src/widgets/editors/previewhelper.h
  32. 1 0
      src/widgets/locationinputwithbrowsebutton.cpp
  33. 30 5
      src/widgets/markdownviewwindow.cpp
  34. 3 1
      src/widgets/markdownviewwindow.h
  35. 10 0
      src/widgets/viewarea.cpp
  36. 6 0
      src/widgets/widgets.pri

+ 1 - 1
libs/vtextedit

@@ -1 +1 @@
-Subproject commit 9b9aa9dd1d8ebec02daee23cb26d0b12ae00cf69
+Subproject commit 5e02a011cb7e4979e897c0670fe8ac8f1060feb4

+ 1 - 1
src/core/configmgr.cpp

@@ -24,7 +24,7 @@
 using namespace vnotex;
 
 #ifndef QT_NO_DEBUG
-// #define VX_DEBUG_WEB
+//    #define VX_DEBUG_WEB
 #endif
 
 const QString ConfigMgr::c_orgName = "VNote";

+ 3 - 1
src/core/configmgr.h

@@ -6,6 +6,8 @@
 #include <QJsonObject>
 #include <QScopedPointer>
 
+#include "noncopyable.h"
+
 namespace vnotex
 {
     class MainConfig;
@@ -14,7 +16,7 @@ namespace vnotex
     class EditorConfig;
     class WidgetConfig;
 
-    class ConfigMgr : public QObject
+    class ConfigMgr : public QObject, private Noncopyable
     {
         Q_OBJECT
     public:

+ 1 - 0
src/core/core.pri

@@ -47,6 +47,7 @@ HEADERS += \
     $$PWD/logger.h \
     $$PWD/mainconfig.h \
     $$PWD/markdowneditorconfig.h \
+    $$PWD/noncopyable.h \
     $$PWD/quickaccesshelper.h \
     $$PWD/singleinstanceguard.h \
     $$PWD/iconfig.h \

+ 3 - 0
src/core/fileopenparameters.h

@@ -11,6 +11,9 @@ namespace vnotex
     {
         ViewWindowMode m_mode = ViewWindowMode::Read;
 
+        // Force to enter m_mode.
+        bool m_forceMode = false;
+
         // Whether focus to the opened window.
         bool m_focus = true;
 

+ 45 - 0
src/core/markdowneditorconfig.cpp

@@ -30,8 +30,15 @@ void MarkdownEditorConfig::init(const QJsonObject &p_app, const QJsonObject &p_u
     loadExportResource(appObj, userObj);
 
     m_webPlantUml = READBOOL(QStringLiteral("web_plantuml"));
+
+    m_plantUmlJar = READSTR(QStringLiteral("plantuml_jar"));
+
+    m_plantUmlCommand = READSTR(QStringLiteral("plantuml_command"));
+
     m_webGraphviz = READBOOL(QStringLiteral("web_graphviz"));
 
+    m_graphvizExe = READSTR(QStringLiteral("graphviz_exe"));
+
     m_prependDotInRelativeLink = READBOOL(QStringLiteral("prepend_dot_in_relative_link"));
     m_confirmBeforeClearObsoleteImages = READBOOL(QStringLiteral("confirm_before_clear_obsolete_images"));
     m_insertFileNameAsTitle = READBOOL(QStringLiteral("insert_file_name_as_title"));
@@ -63,7 +70,10 @@ QJsonObject MarkdownEditorConfig::toJson() const
     obj[QStringLiteral("viewer_resource")] = saveViewerResource();
     obj[QStringLiteral("export_resource")] = saveExportResource();
     obj[QStringLiteral("web_plantuml")] = m_webPlantUml;
+    obj[QStringLiteral("plantuml_jar")] = m_plantUmlJar;
+    obj[QStringLiteral("plantuml_command")] = m_plantUmlCommand;
     obj[QStringLiteral("web_graphviz")] = m_webGraphviz;
+    obj[QStringLiteral("graphviz_exe")] = m_graphvizExe;
     obj[QStringLiteral("prepend_dot_in_relative_link")] = m_prependDotInRelativeLink;
     obj[QStringLiteral("confirm_before_clear_obsolete_images")] = m_confirmBeforeClearObsoleteImages;
     obj[QStringLiteral("insert_file_name_as_title")] = m_insertFileNameAsTitle;
@@ -167,11 +177,46 @@ bool MarkdownEditorConfig::getWebPlantUml() const
     return m_webPlantUml;
 }
 
+void MarkdownEditorConfig::setWebPlantUml(bool p_enabled)
+{
+    updateConfig(m_webPlantUml, p_enabled, this);
+}
+
+const QString &MarkdownEditorConfig::getPlantUmlJar() const
+{
+    return m_plantUmlJar;
+}
+
+void MarkdownEditorConfig::setPlantUmlJar(const QString &p_jar)
+{
+    updateConfig(m_plantUmlJar, p_jar, this);
+}
+
+const QString &MarkdownEditorConfig::getPlantUmlCommand() const
+{
+    return m_plantUmlCommand;
+}
+
 bool MarkdownEditorConfig::getWebGraphviz() const
 {
     return m_webGraphviz;
 }
 
+void MarkdownEditorConfig::setWebGraphviz(bool p_enabled)
+{
+    updateConfig(m_webGraphviz, p_enabled, this);
+}
+
+const QString &MarkdownEditorConfig::getGraphvizExe() const
+{
+    return m_graphvizExe;
+}
+
+void MarkdownEditorConfig::setGraphvizExe(const QString &p_exe)
+{
+    updateConfig(m_graphvizExe, p_exe, this);
+}
+
 bool MarkdownEditorConfig::getPrependDotInRelativeLink() const
 {
     return m_prependDotInRelativeLink;

+ 20 - 0
src/core/markdowneditorconfig.h

@@ -48,8 +48,18 @@ namespace vnotex
         const WebResource &getExportResource() const;
 
         bool getWebPlantUml() const;
+        void setWebPlantUml(bool p_enabled);
+
+        const QString &getPlantUmlJar() const;
+        void setPlantUmlJar(const QString &p_jar);
+
+        const QString &getPlantUmlCommand() const;
 
         bool getWebGraphviz() const;
+        void setWebGraphviz(bool p_enabled);
+
+        const QString &getGraphvizExe() const;
+        void setGraphvizExe(const QString &p_exe);
 
         bool getPrependDotInRelativeLink() const;
 
@@ -124,8 +134,18 @@ namespace vnotex
         // Whether use javascript or external program to render PlantUML.
         bool m_webPlantUml = true;
 
+        // File path of the JAR to render PlantUmL.
+        QString m_plantUmlJar;
+
+        // Command to render PlantUml. If set, will ignore m_plantUmlJar.
+        // %1: the format to render in.
+        QString m_plantUmlCommand;
+
         bool m_webGraphviz = true;
 
+        // Graphviz executable file.
+        QString m_graphvizExe;
+
         // Whether prepend a dot in front of the relative link, like images.
         bool m_prependDotInRelativeLink = false;
 

+ 18 - 0
src/core/noncopyable.h

@@ -0,0 +1,18 @@
+#ifndef NONCOPYABLE_H
+#define NONCOPYABLE_H
+
+namespace vnotex
+{
+    class Noncopyable
+    {
+    protected:
+        Noncopyable() = default;
+
+        virtual ~Noncopyable() = default;
+
+        Noncopyable(const Noncopyable&) = delete;
+        Noncopyable &operator=(const Noncopyable&) = delete;
+    };
+}
+
+#endif // NONCOPYABLE_H

+ 2 - 4
src/core/vnotex.h

@@ -4,6 +4,7 @@
 #include <QObject>
 #include <QScopedPointer>
 
+#include "noncopyable.h"
 #include "thememgr.h"
 #include "global.h"
 
@@ -18,7 +19,7 @@ namespace vnotex
     class Notebook;
     struct ComplexLocation;
 
-    class VNoteX : public QObject
+    class VNoteX : public QObject, private Noncopyable
     {
         Q_OBJECT
     public:
@@ -28,9 +29,6 @@ namespace vnotex
             return inst;
         }
 
-        VNoteX(const VNoteX &) = delete;
-        void operator=(const VNoteX &) = delete;
-
         // MUST be called to load some heavy data.
         // It is good to call it after MainWindow is shown.
         void initLoad();

+ 7 - 0
src/data/core/vnotex.json

@@ -261,8 +261,15 @@
             },
             "//comment" : "Whether use javascript or external program to render PlantUML",
             "web_plantuml" : true,
+            "//commnet" : "Local PlantUML JAR file to render PlantUML",
+            "plantuml_jar" : "",
+            "//commnet" : "Command to render PlantUML via stdin and stdout (overrides plantuml_jar)",
+            "//commnet" : "- %1: the format to render in",
+            "plantuml_command" : "",
             "//comment" : "Whether use javascript or external program to render Graphviz",
             "web_graphviz" : true,
+            "//commnet" : "Local Graphviz executable file to render Graphviz",
+            "graphviz_exe" : "",
             "//comment" : "Whether prepend a dot at front in relative link like images",
             "prepend_dot_in_relative_link" : false,
             "//comment" : "Whether ask for user confirmation before clearing obsolete images",

+ 3 - 0
src/data/extra/themes/moonlight/text-editor.theme

@@ -73,6 +73,9 @@
             "background-color" : "#333842",
             "selected-text-color" : "#e3e5e9",
             "selected-background-color" : "#0c7bff"
+        },
+        "Preview" : {
+            "background-color" : "#b0bec5"
         }
     },
     "markdown-syntax-styles" : {

+ 3 - 2
src/data/extra/web/js/graphrenderer.js

@@ -30,7 +30,7 @@ class GraphRenderer extends VxWorker {
     registerInternal() {
         this.vnotex.on('basicMarkdownRendered', () => {
             this.reset();
-            this.renderCodeNodes(this.vnotex.contentContainer);
+            this.renderCodeNodes();
         });
 
         this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs);
@@ -78,8 +78,9 @@ class GraphRenderer extends VxWorker {
 
     // Interface 2.
     // Get code nodes from markdownIt directly.
-    renderCodeNodes(p_node) {
+    renderCodeNodes() {
         this.nodesToRender = this.vnotex.getWorker('markdownit').getCodeNodes(this.langs);
+        this.numOfRenderedNodes = 0;
         this.doRender();
     }
 

+ 71 - 5
src/data/extra/web/js/graphviz.js

@@ -14,21 +14,30 @@ class Graphviz extends GraphRenderer {
         this.format = 'svg';
 
         this.langs = ['dot'];
+
+        this.useWeb = true;
+
+        this.nextLocalGraphIndex = 1;
     }
 
     registerInternal() {
         this.vnotex.on('basicMarkdownRendered', () => {
             this.reset();
-            this.renderCodeNodes(this.vnotex.contentContainer,
-                                 window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg');
+            this.renderCodeNodes(window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg');
         });
 
         this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs);
+        this.useWeb = window.vxOptions.webGraphviz;
+        if (!this.useWeb) {
+            this.extraScripts = [];
+        }
     }
 
     initialize(p_callback) {
         return super.initialize(() => {
-            this.viz = new Viz();
+            if (this.useWeb) {
+                this.viz = new Viz();
+            }
             p_callback();
         });
     }
@@ -41,13 +50,22 @@ class Graphviz extends GraphRenderer {
     }
 
     // Interface 2.
-    renderCodeNodes(p_node, p_format) {
+    renderCodeNodes(p_format) {
         this.format = p_format;
 
-        super.renderCodeNodes(p_node);
+        super.renderCodeNodes();
     }
 
     renderOne(p_node, p_idx) {
+        if (this.useWeb) {
+            this.renderOnline(p_node, p_idx);
+        } else {
+            this.renderLocal(p_node);
+        }
+    }
+
+    renderOnline(p_node, p_idx) {
+        console.assert(this.viz);
         let func = function(p_graphviz, p_renderNode) {
             let graphviz = p_graphviz;
             let node = p_renderNode;
@@ -85,13 +103,61 @@ class Graphviz extends GraphRenderer {
                 });
 
         }
+
         return true;
     }
 
+    renderLocal(p_node) {
+        let func = function(p_graphviz, p_renderNode) {
+            let graphviz = p_graphviz;
+            let node = p_renderNode;
+            return function(format, data) {
+                if (node && data.length > 0) {
+                    let obj = null;
+                    if (format == 'svg') {
+                        obj = document.createElement('div');
+                        obj.classList.add(graphviz.graphDivClass);
+                        obj.innerHTML = data;
+                        window.vxImageViewer.setupSVGToView(obj.children[0], false);
+                    } else {
+                        obj = document.createElement('div');
+                        obj.classList.add(graphviz.graphDivClass);
+
+                        let imgObj = document.createElement('img');
+                        obj.appendChild(imgObj);
+                        imgObj.src = "data:image/" + format + ";base64, " + data;
+                        window.vxImageViewer.setupIMGToView(imgObj);
+                    }
+
+                    Utils.checkSourceLine(p_node, obj);
+
+                    Utils.replaceNodeWithPreCheck(p_node, obj);
+                }
+                graphviz.finishRenderingOne();
+            };
+        };
+
+        let callback = func(this, p_node);
+        this.vnotex.renderGraph(this.id,
+            this.nextLocalGraphIndex++,
+            this.format,
+            'dot',
+            p_node.textContent,
+            function(id, index, format, data) {
+                callback(format, data);
+            });
+    }
+
     // Render a graph from @p_text in SVG format.
     // p_callback(svgNode).
     renderText(p_text, p_callback) {
+        console.assert(this.useWeb, "renderText() should be called only when web Graphviz is enabled");
+
         let func = () => {
+            if (!this.viz) {
+                console.log("viz is not ready yet");
+                return;
+            }
             this.viz.renderSVGElement(p_text)
                 .then(p_callback)
                 .catch(function(err) {

+ 4 - 0
src/data/extra/web/js/markdownviewer.js

@@ -47,6 +47,10 @@ new QWebChannel(qt.webChannelTransport,
             window.vnotex.saveContent();
         });
 
+        adapter.graphRenderDataReady.connect(function(p_id, p_index, p_format, p_data) {
+            window.vnotex.graphRenderDataReady(p_id, p_index, p_format, p_data);
+        });
+
         console.log('QWebChannel has been set up');
         if (window.vnotex.initialized) {
             window.vnotex.kickOffMarkdown();

+ 43 - 13
src/data/extra/web/js/plantuml.js

@@ -14,16 +14,24 @@ class PlantUml extends GraphRenderer {
         this.format = 'svg';
 
         this.langs = ['plantuml', 'puml'];
+
+        this.useWeb = true;
+
+        this.nextLocalGraphIndex = 1;
     }
 
     registerInternal() {
         this.vnotex.on('basicMarkdownRendered', () => {
             this.reset();
-            this.renderCodeNodes(this.vnotex.contentContainer,
-                                 window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg');
+            this.renderCodeNodes(window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg');
         });
 
         this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs);
+
+        this.useWeb = window.vxOptions.webPlantUml;
+        if (!this.useWeb) {
+            this.extraScripts = [];
+        }
     }
 
 
@@ -35,10 +43,10 @@ class PlantUml extends GraphRenderer {
     }
 
     // Interface 2.
-    renderCodeNodes(p_node, p_format) {
+    renderCodeNodes(p_format) {
         this.format = p_format;
 
-        super.renderCodeNodes(p_node);
+        super.renderCodeNodes();
     }
 
     renderOne(p_node, p_idx) {
@@ -46,20 +54,26 @@ class PlantUml extends GraphRenderer {
             let plantUml = p_plantUml;
             let node = p_node;
             return function(p_format, p_data) {
-                plantUml.handlePlantUmlResult(node, 0, p_format, p_data);
+                plantUml.handlePlantUmlResult(node, p_format, p_data);
             };
         };
 
-        this.renderOnline(this.serverUrl,
-                          this.format,
-                          p_node.textContent,
-                          func(this, p_node));
+        if (this.useWeb) {
+            this.renderOnline(this.serverUrl,
+                              this.format,
+                              p_node.textContent,
+                              func(this, p_node));
+        } else {
+            this.renderLocal(this.format, p_node.textContent, func(this, p_node));
+        }
         return true;
     }
 
     // Render a graph from @p_text in SVG format.
     // p_callback(format, data).
     renderText(p_text, p_callback) {
+        console.assert(this.useWeb, "renderText() should be called only when web PlantUml is enabled");
+
         let func = () => {
             this.renderOnline(this.serverUrl,
                               'svg',
@@ -111,7 +125,19 @@ class PlantUml extends GraphRenderer {
         return url;
     }
 
-    handlePlantUmlResult(p_node, p_timeStamp, p_format, p_result) {
+    // A helper function to render PlantUml via local JAR.
+    renderLocal(p_format, p_text, p_callback) {
+        this.vnotex.renderGraph(this.id,
+            this.nextLocalGraphIndex++,
+            p_format,
+            'puml',
+            p_text,
+            function(id, index, format, data) {
+                p_callback(format, data);
+            });
+    }
+
+    handlePlantUmlResult(p_node, p_format, p_result) {
         if (p_node && p_result.length > 0) {
             let obj = null;
             if (p_format == 'svg') {
@@ -120,9 +146,13 @@ class PlantUml extends GraphRenderer {
                 obj.innerHTML = p_result;
                 window.vxImageViewer.setupSVGToView(obj.children[0], false);
             } else {
-                obj = document.createElement('img');
-                obj.src = "data:image/" + p_format + ";base64, " + p_result;
-                window.vxImageViewer.setupIMGToView(obj);
+                obj = document.createElement('div');
+                obj.classList.add(this.graphDivClass);
+
+                let imgObj = document.createElement('img');
+                obj.appendChild(imgObj);
+                imgObj.src = "data:image/" + p_format + ";base64, " + p_result;
+                window.vxImageViewer.setupIMGToView(imgObj);
             }
 
             Utils.checkSourceLine(p_node, obj);

+ 16 - 0
src/data/extra/web/js/vnotex.js

@@ -39,6 +39,9 @@ class VNoteX extends EventEmitter {
 
         this.sectionNumberBaseLevel = 2;
 
+        // Dict mapping from {id, index} to callback for renderGraph().
+        this.renderGraphCallbacks = {}
+
         window.addEventListener('load', () => {
             console.log('window load finished');
 
@@ -307,6 +310,19 @@ class VNoteX extends EventEmitter {
         }
     }
 
+    renderGraph(p_id, p_index, p_format, p_lang, p_text, p_callback) {
+        this.renderGraphCallbacks[p_id + '_' + p_index] = p_callback;
+        window.vxMarkdownAdapter.renderGraph(p_id, p_index, p_format, p_lang, p_text);
+    }
+
+    graphRenderDataReady(p_id, p_index, p_format, p_data) {
+        let key = p_id + '_' + p_index;
+        if (key in this.renderGraphCallbacks) {
+            this.renderGraphCallbacks[key](p_id, p_index, p_format, p_data);
+            delete this.renderGraphCallbacks[key];
+        }
+    }
+
     static detectOS() {
         let osName="Unknown OS";
         if (navigator.appVersion.indexOf("Win")!=-1) {

+ 5 - 0
src/data/extra/web/js/vxworker.js

@@ -3,6 +3,11 @@ class VxWorker {
     constructor() {
         this.name = '';
         this.vnotex = null;
+
+        if (!window.vxWorkerId) {
+            window.vxWorkerId = 1;
+        }
+        this.id = window.vxWorkerId++;
     }
 
     // Called when registering this worker.

+ 148 - 3
src/widgets/dialogs/settings/markdowneditorpage.cpp

@@ -8,6 +8,8 @@
 #include <QComboBox>
 #include <QSpinBox>
 #include <QHBoxLayout>
+#include <QPushButton>
+#include <QFileDialog>
 
 #include <widgets/widgetsfactory.h>
 #include <core/editorconfig.h>
@@ -16,6 +18,10 @@
 #include <utils/widgetutils.h>
 
 #include "editorpage.h"
+#include <widgets/locationinputwithbrowsebutton.h>
+#include <widgets/messageboxhelper.h>
+#include <widgets/editors/plantumlhelper.h>
+#include <widgets/editors/graphvizhelper.h>
 
 using namespace vnotex;
 
@@ -76,6 +82,20 @@ void MarkdownEditorPage::loadInternal()
     m_smartTableCheckBox->setChecked(markdownConfig.getSmartTableEnabled());
 
     m_spellCheckCheckBox->setChecked(markdownConfig.isSpellCheckEnabled());
+
+    {
+        int idx = m_plantUmlModeComboBox->findData(markdownConfig.getWebPlantUml() ? 0 : 1);
+        m_plantUmlModeComboBox->setCurrentIndex(idx);
+    }
+
+    m_plantUmlJarFileInput->setText(markdownConfig.getPlantUmlJar());
+
+    {
+        int idx = m_graphvizModeComboBox->findData(markdownConfig.getWebGraphviz() ? 0 : 1);
+        m_graphvizModeComboBox->setCurrentIndex(idx);
+    }
+
+    m_graphvizFileInput->setText(markdownConfig.getGraphvizExe());
 }
 
 void MarkdownEditorPage::saveInternal()
@@ -118,6 +138,14 @@ void MarkdownEditorPage::saveInternal()
 
     markdownConfig.setSpellCheckEnabled(m_spellCheckCheckBox->isChecked());
 
+    markdownConfig.setWebPlantUml(m_plantUmlModeComboBox->currentData().toInt() == 0);
+
+    markdownConfig.setPlantUmlJar(m_plantUmlJarFileInput->text());
+
+    markdownConfig.setWebGraphviz(m_graphvizModeComboBox->currentData().toInt() == 0);
+
+    markdownConfig.setGraphvizExe(m_graphvizFileInput->text());
+
     EditorPage::notifyEditorConfigChange();
 }
 
@@ -264,7 +292,7 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup()
     {
         auto sectionLayout = new QHBoxLayout();
 
-        m_sectionNumberComboBox = WidgetsFactory::createComboBox(this);
+        m_sectionNumberComboBox = WidgetsFactory::createComboBox(box);
         m_sectionNumberComboBox->setToolTip(tr("Section number mode"));
         m_sectionNumberComboBox->addItem(tr("None"), (int)MarkdownEditorConfig::SectionNumberMode::None);
         m_sectionNumberComboBox->addItem(tr("Read"), (int)MarkdownEditorConfig::SectionNumberMode::Read);
@@ -273,7 +301,7 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup()
         connect(m_sectionNumberComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
                 this, &MarkdownEditorPage::pageIsChanged);
 
-        m_sectionNumberBaseLevelSpinBox = WidgetsFactory::createSpinBox(this);
+        m_sectionNumberBaseLevelSpinBox = WidgetsFactory::createSpinBox(box);
         m_sectionNumberBaseLevelSpinBox->setToolTip(tr("Base level to start section numbering in edit mode"));
         m_sectionNumberBaseLevelSpinBox->setRange(1, 6);
         m_sectionNumberBaseLevelSpinBox->setSingleStep(1);
@@ -281,7 +309,7 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup()
         connect(m_sectionNumberBaseLevelSpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
                 this, &MarkdownEditorPage::pageIsChanged);
 
-        m_sectionNumberStyleComboBox = WidgetsFactory::createComboBox(this);
+        m_sectionNumberStyleComboBox = WidgetsFactory::createComboBox(box);
         m_sectionNumberStyleComboBox->setToolTip(tr("Section number style"));
         m_sectionNumberStyleComboBox->addItem(tr("1.1."), (int)MarkdownEditorConfig::SectionNumberStyle::DigDotDigDot);
         m_sectionNumberStyleComboBox->addItem(tr("1.1"), (int)MarkdownEditorConfig::SectionNumberStyle::DigDotDig);
@@ -300,5 +328,122 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup()
         addSearchItem(label, m_sectionNumberComboBox->toolTip(), m_sectionNumberComboBox);
     }
 
+    {
+        m_plantUmlModeComboBox = WidgetsFactory::createComboBox(box);
+        m_plantUmlModeComboBox->setToolTip(tr("Use online service or local JAR file to render PlantUml graphs"));
+
+        m_plantUmlModeComboBox->addItem(tr("Online Service"), 0);
+        m_plantUmlModeComboBox->addItem(tr("Local JAR"), 1);
+
+        const QString label(tr("PlantUml:"));
+        layout->addRow(label, m_plantUmlModeComboBox);
+        addSearchItem(label, m_plantUmlModeComboBox->toolTip(), m_plantUmlModeComboBox);
+        connect(m_plantUmlModeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
+                this, &MarkdownEditorPage::pageIsChanged);
+    }
+
+    {
+        auto jarLayout = new QHBoxLayout();
+
+        m_plantUmlJarFileInput = new LocationInputWithBrowseButton(box);
+        m_plantUmlJarFileInput->setToolTip(tr("Local JAR file to render PlantUML graphs"));
+        connect(m_plantUmlJarFileInput, &LocationInputWithBrowseButton::clicked,
+                this, [this]() {
+                    auto filePath = QFileDialog::getOpenFileName(this,
+                                                                 tr("Select PlantUml JAR File"),
+                                                                 QDir::homePath(),
+                                                                 "PlantUml JAR (*.jar)");
+                    if (!filePath.isEmpty()) {
+                        m_plantUmlJarFileInput->setText(filePath);
+                    }
+                });
+        jarLayout->addWidget(m_plantUmlJarFileInput, 1);
+
+        auto testBtn = new QPushButton(tr("Test"), box);
+        testBtn->setToolTip(tr("Test PlantUml JAR and Java Runtime Environment"));
+        connect(testBtn, &QPushButton::clicked,
+                this, [this]() {
+                    const auto jar = m_plantUmlJarFileInput->text();
+                    if (jar.isEmpty() || !QFileInfo::exists(jar)) {
+                        MessageBoxHelper::notify(MessageBoxHelper::Warning,
+                                                 tr("The JAR file (%1) specified does not exist.").arg(jar),
+                                                 this);
+                        return;
+                    }
+
+                    auto testRet = PlantUmlHelper::testPlantUml(jar);
+                    MessageBoxHelper::notify(MessageBoxHelper::Information,
+                                             tr("Test %1.").arg(testRet.first ? tr("succeeded") : tr("failed")),
+                                             QString(),
+                                             testRet.second,
+                                             this);
+                });
+        jarLayout->addWidget(testBtn);
+
+        const QString label(tr("PlantUml JAR file:"));
+        layout->addRow(label, jarLayout);
+        addSearchItem(label, m_plantUmlJarFileInput->toolTip(), m_plantUmlJarFileInput);
+        connect(m_plantUmlJarFileInput, &LocationInputWithBrowseButton::textChanged,
+                this, &MarkdownEditorPage::pageIsChanged);
+    }
+
+    {
+        m_graphvizModeComboBox = WidgetsFactory::createComboBox(box);
+        m_graphvizModeComboBox->setToolTip(tr("Use online service or local executable file to render Graphviz graphs"));
+
+        m_graphvizModeComboBox->addItem(tr("Online Service"), 0);
+        m_graphvizModeComboBox->addItem(tr("Local Executable"), 1);
+
+        const QString label(tr("Graphviz:"));
+        layout->addRow(label, m_graphvizModeComboBox);
+        addSearchItem(label, m_graphvizModeComboBox->toolTip(), m_graphvizModeComboBox);
+        connect(m_graphvizModeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
+                this, &MarkdownEditorPage::pageIsChanged);
+    }
+
+    {
+        auto fileLayout = new QHBoxLayout();
+
+        m_graphvizFileInput = new LocationInputWithBrowseButton(box);
+        m_graphvizFileInput->setToolTip(tr("Local executable file to render Graphviz graphs"));
+        connect(m_graphvizFileInput, &LocationInputWithBrowseButton::clicked,
+                this, [this]() {
+                    auto filePath = QFileDialog::getOpenFileName(this,
+                                                                 tr("Select Graphviz Executable File"),
+                                                                 QDir::homePath());
+                    if (!filePath.isEmpty()) {
+                        m_graphvizFileInput->setText(filePath);
+                    }
+                });
+        fileLayout->addWidget(m_graphvizFileInput, 1);
+
+        auto testBtn = new QPushButton(tr("Test"), box);
+        testBtn->setToolTip(tr("Test Graphviz executable file"));
+        connect(testBtn, &QPushButton::clicked,
+                this, [this]() {
+                    const auto exe = m_graphvizFileInput->text();
+                    if (exe.isEmpty() || !QFileInfo::exists(exe)) {
+                        MessageBoxHelper::notify(MessageBoxHelper::Warning,
+                                                 tr("The executable file (%1) specified does not exist.").arg(exe),
+                                                 this);
+                        return;
+                    }
+
+                    auto testRet = GraphvizHelper::testGraphviz(exe);
+                    MessageBoxHelper::notify(MessageBoxHelper::Information,
+                                             tr("Test %1.").arg(testRet.first ? tr("succeeded") : tr("failed")),
+                                             QString(),
+                                             testRet.second,
+                                             this);
+                });
+        fileLayout->addWidget(testBtn);
+
+        const QString label(tr("Graphviz executable file:"));
+        layout->addRow(label, fileLayout);
+        addSearchItem(label, m_graphvizFileInput->toolTip(), m_graphvizFileInput);
+        connect(m_graphvizFileInput, &LocationInputWithBrowseButton::textChanged,
+                this, &MarkdownEditorPage::pageIsChanged);
+    }
+
     return box;
 }

+ 10 - 0
src/widgets/dialogs/settings/markdowneditorpage.h

@@ -11,6 +11,8 @@ class QComboBox;
 
 namespace vnotex
 {
+    class LocationInputWithBrowseButton;
+
     class MarkdownEditorPage : public SettingsPage
     {
         Q_OBJECT
@@ -60,6 +62,14 @@ namespace vnotex
         QCheckBox *m_smartTableCheckBox = nullptr;
 
         QCheckBox *m_spellCheckCheckBox = nullptr;
+
+        QComboBox *m_plantUmlModeComboBox = nullptr;
+
+        LocationInputWithBrowseButton *m_plantUmlJarFileInput = nullptr;
+
+        QComboBox *m_graphvizModeComboBox = nullptr;
+
+        LocationInputWithBrowseButton *m_graphvizFileInput = nullptr;
     };
 }
 

+ 201 - 0
src/widgets/editors/graphhelper.cpp

@@ -0,0 +1,201 @@
+#include "graphhelper.h"
+
+#include <QDebug>
+#include <QFileInfo>
+
+#include <utils/processutils.h>
+
+using namespace vnotex;
+
+#define TaskIdProperty "GraphTaskId"
+#define TaskTimeStampProperty "GraphTaskTimeStamp"
+
+GraphHelper::GraphHelper()
+    : m_cache(100, CacheItem())
+{
+}
+
+QStringList GraphHelper::getArgsToUse(const QStringList &p_args)
+{
+    if (p_args.isEmpty()) {
+        return QStringList();
+    }
+
+    if (p_args[0] == "-c") {
+        // Combine all the arguments except the first one.
+        QStringList args;
+        args << p_args[0];
+
+        QString subCmd;
+        for (int i = 1; i < p_args.size(); ++i) {
+            subCmd += " " + p_args[i];
+        }
+        args << subCmd;
+
+        return args;
+    } else {
+        return p_args;
+    }
+}
+
+void GraphHelper::process(quint64 p_id,
+                          TimeStamp p_timeStamp,
+                          const QString &p_format,
+                          const QString &p_text,
+                          const ResultCallback &p_callback)
+{
+    Task task;
+    task.m_id = p_id;
+    task.m_timeStamp = p_timeStamp;
+    task.m_format = p_format;
+    task.m_text = p_text;
+    task.m_callback = p_callback;
+
+    m_tasks.enqueue(task);
+
+    processOneTask();
+}
+
+void GraphHelper::processOneTask()
+{
+    if (m_taskOngoing || m_tasks.isEmpty()) {
+        return;
+    }
+
+    m_taskOngoing = true;
+
+    const auto &task = m_tasks.head();
+
+    const auto &cachedData = m_cache.get(task.m_text);
+    if (!cachedData.isNull() && cachedData.m_format == task.m_format) {
+        finishOneTask(cachedData.m_data);
+        return;
+    }
+
+    if (!m_programValid) {
+        qWarning() << "program to execute for rendering is not valid" << m_program;
+        finishOneTask(QString());
+        return;
+    }
+
+    // Will be released in finishOneTask.
+    QProcess *process = new QProcess();
+    process->setProperty(TaskIdProperty, task.m_id);
+    process->setProperty(TaskTimeStampProperty, task.m_timeStamp);
+    QObject::connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
+                     [this, process](int exitCode, QProcess::ExitStatus exitStatus) {
+                         finishOneTask(process, exitCode, exitStatus);
+                     });
+
+    if (m_overriddenCommand.isEmpty()) {
+        Q_ASSERT(!m_program.isEmpty());
+        QStringList args(m_args);
+        args << getFormatArgs(task.m_format);
+        process->start(m_program, getArgsToUse(args));
+    } else {
+        auto cmd = getCommandToUse(m_overriddenCommand, task.m_format);
+        process->start(cmd);
+    }
+
+    if (process->write(task.m_text.toUtf8()) == -1) {
+        qWarning() << "Graph task" << task.m_id << "failed to write to process stdin:" << process->errorString();
+    }
+
+    process->closeWriteChannel();
+}
+
+void GraphHelper::finishOneTask(QProcess *p_process, int p_exitCode, QProcess::ExitStatus p_exitStatus)
+{
+    Q_ASSERT(m_taskOngoing && !m_tasks.isEmpty());
+
+    const auto task = m_tasks.dequeue();
+
+    const quint64 id = p_process->property(TaskIdProperty).toULongLong();
+    const quint64 timeStamp = p_process->property(TaskTimeStampProperty).toULongLong();
+    Q_ASSERT(task.m_id == id && task.m_timeStamp == timeStamp);
+
+    qDebug() << "Graph task" << id << timeStamp << "finished";
+
+    bool failed = true;
+    if (p_exitStatus == QProcess::NormalExit) {
+        if (p_exitCode < 0) {
+            qWarning() << "Graph task" << id << "failed:" << p_exitCode;
+        } else {
+            failed = false;
+            const auto outBa = p_process->readAllStandardOutput();
+            QString data;
+            if (task.m_format == QStringLiteral("svg")) {
+                data = QString::fromLocal8Bit(outBa);
+                task.m_callback(id, timeStamp, task.m_format, data);
+            } else {
+                data = QString::fromLocal8Bit(outBa.toBase64());
+                task.m_callback(id, timeStamp, task.m_format, data);
+            }
+
+            CacheItem item;
+            item.m_format = task.m_format;
+            item.m_data = data;
+            m_cache.set(task.m_text, item);
+        }
+    } else {
+        qWarning() << "Graph task" << id << "failed to start" << p_exitCode << p_exitStatus;
+    }
+
+    const QByteArray errBa = p_process->readAllStandardError();
+    if (!errBa.isEmpty()) {
+        QString errStr(QString::fromLocal8Bit(errBa));
+        if (failed) {
+            qWarning() << "Graph task" << id << "stderr:" << errStr;
+        } else {
+            qDebug() << "Graph task" << id << "stderr:" << errStr;
+        }
+    }
+
+    if (failed) {
+        task.m_callback(id, task.m_timeStamp, task.m_format, QString());
+    }
+
+    p_process->deleteLater();
+
+    m_taskOngoing = false;
+    processOneTask();
+}
+
+void GraphHelper::finishOneTask(const QString &p_data)
+{
+    Q_ASSERT(m_taskOngoing && !m_tasks.isEmpty());
+
+    const auto task = m_tasks.dequeue();
+
+    qDebug() << "Graph task" << task.m_id << task.m_timeStamp << "finished by cache" << p_data.size();
+
+    task.m_callback(task.m_id, task.m_timeStamp, task.m_format, p_data);
+
+    m_taskOngoing = false;
+    processOneTask();
+}
+
+QString GraphHelper::getCommandToUse(const QString &p_command, const QString &p_format)
+{
+    auto cmd(p_command);
+    cmd.replace("%1", p_format);
+    return cmd;
+}
+
+void GraphHelper::clearCache()
+{
+    m_cache.clear();
+}
+
+void GraphHelper::checkValidProgram()
+{
+    m_programValid = true;
+    if (m_overriddenCommand.isEmpty()) {
+        if (m_program.isEmpty()) {
+            m_programValid = false;
+        } else {
+            QFileInfo finfo(m_program);
+            m_programValid = !finfo.isAbsolute() || finfo.isExecutable();
+        }
+    }
+}

+ 91 - 0
src/widgets/editors/graphhelper.h

@@ -0,0 +1,91 @@
+#ifndef GRAPHHELPER_H
+#define GRAPHHELPER_H
+
+#include <QProcess>
+#include <QStringList>
+#include <QPair>
+#include <QQueue>
+
+#include <core/noncopyable.h>
+#include <core/global.h>
+#include <vtextedit/lrucache.h>
+
+namespace vnotex
+{
+    class GraphHelper : private Noncopyable
+    {
+    public:
+        typedef std::function<void(quint64, TimeStamp, const QString &, const QString &)> ResultCallback;
+
+        GraphHelper();
+
+        void process(quint64 p_id,
+                     TimeStamp p_timeStamp,
+                     const QString &p_format,
+                     const QString &p_text,
+                     const ResultCallback &p_callback);
+
+    protected:
+        virtual QStringList getFormatArgs(const QString &p_format) = 0;
+
+        void clearCache();
+
+        void checkValidProgram();
+
+        static QStringList getArgsToUse(const QStringList &p_args);
+
+        static QString getCommandToUse(const QString &p_command,
+                                       const QString &p_format);
+
+        QString m_program;
+
+        QStringList m_args;
+
+        // If this is not empty, @m_program and @m_args will be ignored.
+        QString m_overriddenCommand;
+
+    private:
+        struct Task
+        {
+            quint64 m_id = 0;
+
+            TimeStamp m_timeStamp = 0;
+
+            QString m_format;
+
+            QString m_text;
+
+            ResultCallback m_callback;
+        };
+
+        struct CacheItem
+        {
+            bool isNull() const
+            {
+                return m_data.isNull();
+            }
+
+            QString m_format;
+
+            QString m_data;
+        };
+
+        void processOneTask();
+
+        void finishOneTask(QProcess *p_process, int p_exitCode, QProcess::ExitStatus p_exitStatus);
+
+        void finishOneTask(const QString &p_data);
+
+        QQueue<Task> m_tasks;
+
+        bool m_taskOngoing = false;
+
+        // {text} -> CacheItem.
+        vte::LruCache<QString, CacheItem> m_cache;
+
+        // Whether @m_program is valid.
+        bool m_programValid = true;
+    };
+}
+
+#endif // GRAPHHELPER_H

+ 74 - 0
src/widgets/editors/graphvizhelper.cpp

@@ -0,0 +1,74 @@
+#include "graphvizhelper.h"
+
+#include <QDebug>
+
+#include <utils/processutils.h>
+
+using namespace vnotex;
+
+void GraphvizHelper::init(const QString &p_graphvizFile)
+{
+    if (m_initialized) {
+        return;
+    }
+
+    m_initialized = true;
+
+    update(p_graphvizFile);
+}
+
+void GraphvizHelper::update(const QString &p_graphvizFile)
+{
+    if (!m_initialized) {
+        return;
+    }
+
+    prepareProgramAndArgs(p_graphvizFile, m_program, m_args);
+
+    checkValidProgram();
+
+    clearCache();
+}
+
+void GraphvizHelper::prepareProgramAndArgs(const QString &p_graphvizFile,
+                                           QString &p_program,
+                                           QStringList &p_args)
+{
+    p_program = p_graphvizFile.isEmpty() ? QStringLiteral("dot") : p_graphvizFile;
+    p_args.clear();
+}
+
+QPair<bool, QString> GraphvizHelper::testGraphviz(const QString &p_graphvizFile)
+{
+    auto ret = qMakePair(false, QString());
+
+    QString program;
+    QStringList args;
+    prepareProgramAndArgs(p_graphvizFile, program, args);
+    args << "-Tsvg";
+
+    const QString testGraph("digraph G {VNote->Markdown}");
+
+    int exitCode = -1;
+    QByteArray outData;
+    QByteArray errData;
+    auto state = ProcessUtils::start(program,
+                                     args,
+                                     testGraph.toUtf8(),
+                                     exitCode,
+                                     outData,
+                                     errData);
+    ret.first = (state == ProcessUtils::Succeeded) && (exitCode == 0);
+
+    ret.second = QString("%1 %2\n\nExitcode: %3\n\nOutput: %4\n\nError: %5")
+                        .arg(program, args.join(' '), QString::number(exitCode), QString::fromLocal8Bit(outData), QString::fromLocal8Bit(errData));
+
+    return ret;
+}
+
+QStringList GraphvizHelper::getFormatArgs(const QString &p_format)
+{
+    QStringList args;
+    args << ("-T" + p_format);
+    return args;
+}

+ 36 - 0
src/widgets/editors/graphvizhelper.h

@@ -0,0 +1,36 @@
+#ifndef GRAPHVIZHELPER_H
+#define GRAPHVIZHELPER_H
+
+#include "graphhelper.h"
+
+namespace vnotex
+{
+    class GraphvizHelper : public GraphHelper
+    {
+    public:
+        void init(const QString &p_graphvizFile);
+
+        void update(const QString &p_graphvizFile);
+
+        static GraphvizHelper &getInst()
+        {
+            static GraphvizHelper inst;
+            return inst;
+        }
+
+        static QPair<bool, QString> testGraphviz(const QString &p_graphvizFile);
+
+    private:
+        GraphvizHelper() = default;
+
+        QStringList getFormatArgs(const QString &p_format) Q_DECL_OVERRIDE;
+
+        static void prepareProgramAndArgs(const QString &p_graphvizFile,
+                                          QString &p_program,
+                                          QStringList &p_args);
+
+        bool m_initialized = false;
+    };
+}
+
+#endif // GRAPHVIZHELPER_H

+ 8 - 0
src/widgets/editors/markdowneditor.cpp

@@ -21,6 +21,7 @@
 #include <vtextedit/vtextedit.h>
 #include <vtextedit/texteditutils.h>
 #include <vtextedit/networkutils.h>
+#include <vtextedit/theme.h>
 
 #include <widgets/dialogs/linkinsertdialog.h>
 #include <widgets/dialogs/imageinsertdialog.h>
@@ -1274,3 +1275,10 @@ void MarkdownEditor::setupTableHelper()
     connect(getHighlighter(), &vte::PegMarkdownHighlighter::tableBlocksUpdated,
             m_tableHelper, &MarkdownTableHelper::updateTableBlocks);
 }
+
+QRgb MarkdownEditor::getPreviewBackground() const
+{
+    auto th = theme();
+    const auto &fmt = th->editorStyle(vte::Theme::EditorStyle::Preview);
+    return fmt.m_backgroundColor;
+}

+ 2 - 0
src/widgets/editors/markdowneditor.h

@@ -100,6 +100,8 @@ namespace vnotex
 
         void updateFromConfig(bool p_initialized = true);
 
+        QRgb getPreviewBackground() const;
+
     public slots:
         void handleHtmlToMarkdownData(quint64 p_id, TimeStamp p_timeStamp, const QString &p_text);
 

+ 34 - 0
src/widgets/editors/markdownvieweradapter.cpp

@@ -4,6 +4,8 @@
 #include <QMap>
 
 #include "../outlineprovider.h"
+#include "plantumlhelper.h"
+#include "graphvizhelper.h"
 
 using namespace vnotex;
 
@@ -371,3 +373,35 @@ void MarkdownViewerAdapter::reset()
     m_currentHeadingIndex = -1;
     m_crossCopyTargets.clear();
 }
+
+void MarkdownViewerAdapter::renderGraph(quint64 p_id,
+                                        quint64 p_index,
+                                        const QString &p_format,
+                                        const QString &p_lang,
+                                        const QString &p_text)
+{
+    if (p_text.isEmpty()) {
+        emit graphRenderDataReady(p_id, p_index, p_format, QString());
+        return;
+    }
+
+    if (p_lang == QStringLiteral("puml")) {
+        PlantUmlHelper::getInst().process(p_id,
+                                          p_index,
+                                          p_format,
+                                          p_text,
+                                          [this](quint64 id, TimeStamp timeStamp, const QString &format, const QString &data) {
+                                              emit graphRenderDataReady(id, timeStamp, format, data);
+                                          });
+    } else if (p_lang == QStringLiteral("dot")) {
+        GraphvizHelper::getInst().process(p_id,
+                                          p_index,
+                                          p_format,
+                                          p_text,
+                                          [this](quint64 id, TimeStamp timeStamp, const QString &format, const QString &data) {
+                                              emit graphRenderDataReady(id, timeStamp, format, data);
+                                          });
+    } else {
+        Q_ASSERT(false);
+    }
+}

+ 12 - 0
src/widgets/editors/markdownvieweradapter.h

@@ -172,6 +172,13 @@ namespace vnotex
 
         void setSavedContent(const QString &p_headContent, const QString &p_styleContent, const QString &p_content, const QString &p_bodyClassList);
 
+        // Call local CPP code to render graph.
+        void renderGraph(quint64 p_id,
+                         quint64 p_index,
+                         const QString &p_format,
+                         const QString &p_lang,
+                         const QString &p_text);
+
         // Signals to be connected at web side.
     signals:
         // Current Markdown text is updated.
@@ -208,6 +215,11 @@ namespace vnotex
         // Request to get the whole HTML content.
         void contentRequested();
 
+        void graphRenderDataReady(quint64 p_id,
+                                  quint64 p_index,
+                                  const QString &p_format,
+                                  const QString &p_data);
+
     // Signals to be connected at cpp side.
     signals:
         void graphPreviewDataReady(const PreviewData &p_data);

+ 104 - 0
src/widgets/editors/plantumlhelper.cpp

@@ -0,0 +1,104 @@
+#include "plantumlhelper.h"
+
+#include <QDebug>
+
+#include <utils/processutils.h>
+
+using namespace vnotex;
+
+void PlantUmlHelper::init(const QString &p_plantUmlJarFile,
+                          const QString &p_graphvizFile,
+                          const QString &p_overriddenCommand)
+{
+    if (m_initialized) {
+        return;
+    }
+
+    m_initialized = true;
+
+    update(p_plantUmlJarFile, p_graphvizFile, p_overriddenCommand);
+}
+
+void PlantUmlHelper::update(const QString &p_plantUmlJarFile,
+                            const QString &p_graphvizFile,
+                            const QString &p_overriddenCommand)
+{
+    if (!m_initialized) {
+        return;
+    }
+
+    m_overriddenCommand = p_overriddenCommand;
+    if (m_overriddenCommand.isEmpty()) {
+        prepareProgramAndArgs(p_plantUmlJarFile, p_graphvizFile, m_program, m_args);
+    } else {
+        m_program.clear();
+        m_args.clear();
+    }
+
+    checkValidProgram();
+
+    clearCache();
+}
+
+void PlantUmlHelper::prepareProgramAndArgs(const QString &p_plantUmlJarFile,
+                                           const QString &p_graphvizFile,
+                                           QString &p_program,
+                                           QStringList &p_args)
+{
+#if defined(Q_OS_WIN)
+    p_program = "java";
+#else
+    p_program = "/bin/sh";
+    p_args << "-c";
+    p_args << "java";
+#endif
+
+    p_args << "-Djava.awt.headless=true";
+
+    p_args << "-jar" << p_plantUmlJarFile;
+
+    p_args << "-charset" << "UTF-8";
+
+    if (!p_graphvizFile.isEmpty()) {
+        p_args << "-graphvizdot" << p_graphvizFile;
+    }
+
+    p_args << "-pipe";
+}
+
+QPair<bool, QString> PlantUmlHelper::testPlantUml(const QString &p_plantUmlJarFile)
+{
+    auto ret = qMakePair(false, QString());
+
+    QString program;
+    QStringList args;
+    prepareProgramAndArgs(p_plantUmlJarFile, QString(), program, args);
+
+    args << "-tsvg";
+    args = getArgsToUse(args);
+
+    const QString testGraph("VNote->Markdown : Hello");
+
+    int exitCode = -1;
+    QByteArray outData;
+    QByteArray errData;
+    auto state = ProcessUtils::start(program,
+                                     args,
+                                     testGraph.toUtf8(),
+                                     exitCode,
+                                     outData,
+                                     errData);
+    ret.first = (state == ProcessUtils::Succeeded) && (exitCode == 0);
+
+    ret.second = QString("%1 %2\n\nExitcode: %3\n\nOutput: %4\n\nError: %5")
+                        .arg(program, args.join(' '), QString::number(exitCode), QString::fromLocal8Bit(outData), QString::fromLocal8Bit(errData));
+
+    return ret;
+}
+
+QStringList PlantUmlHelper::getFormatArgs(const QString &p_format)
+{
+    QStringList args;
+    args << ("-t" + p_format);
+    return args;
+}

+ 41 - 0
src/widgets/editors/plantumlhelper.h

@@ -0,0 +1,41 @@
+#ifndef PLANTUMLHELPER_H
+#define PLANTUMLHELPER_H
+
+#include "graphhelper.h"
+
+namespace vnotex
+{
+    class PlantUmlHelper : public GraphHelper
+    {
+    public:
+        void init(const QString &p_plantUmlJarFile,
+                  const QString &p_graphvizFile,
+                  const QString &p_overriddenCommand);
+
+        void update(const QString &p_plantUmlJarFile,
+                    const QString &p_graphvizFile,
+                    const QString &p_overriddenCommand);
+
+        static PlantUmlHelper &getInst()
+        {
+            static PlantUmlHelper inst;
+            return inst;
+        }
+
+        static QPair<bool, QString> testPlantUml(const QString &p_plantUmlJarFile);
+
+    private:
+        PlantUmlHelper() = default;
+
+        QStringList getFormatArgs(const QString &p_format) Q_DECL_OVERRIDE;
+
+        static void prepareProgramAndArgs(const QString &p_plantUmlJarFile,
+                                          const QString &p_graphvizFile,
+                                          QString &p_program,
+                                          QStringList &p_args);
+
+        bool m_initialized = false;
+    };
+}
+
+#endif // PLANTUMLHELPER_H

+ 95 - 10
src/widgets/editors/previewhelper.cpp

@@ -12,6 +12,8 @@
 #include <utils/utils.h>
 
 #include "markdowneditor.h"
+#include "plantumlhelper.h"
+#include "graphvizhelper.h"
 
 using namespace vnotex;
 
@@ -134,9 +136,11 @@ void PreviewHelper::codeBlocksUpdated(vte::TimeStamp p_timeStamp,
     ++m_codeBlockTimeStamp;
     m_codeBlocksData.clear();
 
-    bool needUpdateEditorInplacePreview = true;
+    QVector<int> needPreviewBlocks;
+
+    for (int i = 0; i < p_codeBlocks.size(); ++i) {
+        const auto &cb = p_codeBlocks[i];
 
-    for (const auto &cb : p_codeBlocks) {
         const auto needPreview = isLangNeedPreview(cb.m_lang);
         if (!needPreview.first && !needPreview.second) {
             continue;
@@ -158,16 +162,16 @@ void PreviewHelper::codeBlocksUpdated(vte::TimeStamp p_timeStamp,
         }
 
         if (m_inplacePreviewEnabled && needPreview.first && !cacheHit) {
-            // No need to update in-place preview for now.
-            needUpdateEditorInplacePreview = false;
             m_codeBlocksData[blockPreviewIdx].m_text = cb.m_text;
-            inplacePreviewCodeBlock(blockPreviewIdx);
+            needPreviewBlocks.push_back(blockPreviewIdx);
         }
     }
 
-    if (needUpdateEditorInplacePreview) {
-        updateEditorInplacePreviewCodeBlock();
+    for (auto idx : needPreviewBlocks) {
+        inplacePreviewCodeBlock(idx);
     }
+
+    updateEditorInplacePreviewCodeBlock();
 }
 
 bool PreviewHelper::checkPreviewSourceLang(SourceFlag p_flag, const QString &p_lang) const
@@ -221,13 +225,38 @@ void PreviewHelper::inplacePreviewCodeBlock(int p_blockPreviewIdx)
     if (checkPreviewSourceLang(SourceFlag::FlowChart, blockData.m_lang)
         || checkPreviewSourceLang(SourceFlag::WaveDrom, blockData.m_lang)
         || checkPreviewSourceLang(SourceFlag::Mermaid, blockData.m_lang)
-        || checkPreviewSourceLang(SourceFlag::PlantUml, blockData.m_lang)
-        || checkPreviewSourceLang(SourceFlag::Graphviz, blockData.m_lang)
+        || (checkPreviewSourceLang(SourceFlag::PlantUml, blockData.m_lang) && m_webPlantUmlEnabled)
+        || (checkPreviewSourceLang(SourceFlag::Graphviz, blockData.m_lang) && m_webGraphvizEnabled)
         || checkPreviewSourceLang(SourceFlag::Math, blockData.m_lang)) {
         emit graphPreviewRequested(p_blockPreviewIdx,
                                    m_codeBlockTimeStamp,
                                    blockData.m_lang,
                                    TextUtils::removeCodeBlockFence(blockData.m_text));
+        return;
+    }
+
+    if (!m_webPlantUmlEnabled && checkPreviewSourceLang(SourceFlag::PlantUml, blockData.m_lang)) {
+        // Local PlantUml.
+        PlantUmlHelper::getInst().process(static_cast<quint64>(p_blockPreviewIdx),
+                                          m_codeBlockTimeStamp,
+                                          QStringLiteral("svg"),
+                                          TextUtils::removeCodeBlockFence(blockData.m_text),
+                                          [this](quint64 id, TimeStamp timeStamp, const QString &format, const QString &data) {
+                                              handleLocalData(id, timeStamp, format, data, true);
+                                          });
+        return;
+    }
+
+    if (!m_webGraphvizEnabled && checkPreviewSourceLang(SourceFlag::Graphviz, blockData.m_lang)) {
+        // Local PlantUml.
+        GraphvizHelper::getInst().process(static_cast<quint64>(p_blockPreviewIdx),
+                                          m_codeBlockTimeStamp,
+                                          QStringLiteral("svg"),
+                                          TextUtils::removeCodeBlockFence(blockData.m_text),
+                                          [this](quint64 id, TimeStamp timeStamp, const QString &format, const QString &data) {
+                                              handleLocalData(id, timeStamp, format, data, false);
+                                          });
+        return;
     }
 }
 
@@ -242,10 +271,11 @@ void PreviewHelper::handleGraphPreviewData(const MarkdownViewerAdapter::PreviewD
     }
 
     auto &blockData = m_codeBlocksData[p_data.m_id];
+    const bool forcedBackground = needForcedBackground(blockData.m_lang);
     auto previewData = QSharedPointer<GraphPreviewData>::create(p_data.m_timeStamp,
                                                                 p_data.m_format,
                                                                 p_data.m_data,
-                                                                0,
+                                                                forcedBackground ? m_editor->getPreviewBackground() : 0,
                                                                 p_data.m_needScale ? getEditorScaleFactor() : 1);
     m_codeBlockCache.set(blockData.m_text, previewData);
     blockData.m_text.clear();
@@ -414,3 +444,58 @@ qreal PreviewHelper::getEditorScaleFactor() const
 
     return 1;
 }
+
+void PreviewHelper::setWebPlantUmlEnabled(bool p_enabled)
+{
+    m_webPlantUmlEnabled = p_enabled;
+}
+
+void PreviewHelper::setWebGraphvizEnabled(bool p_enabled)
+{
+    m_webGraphvizEnabled = p_enabled;
+}
+
+void PreviewHelper::handleLocalData(quint64 p_id,
+                                    TimeStamp p_timeStamp,
+                                    const QString &p_format,
+                                    const QString &p_data,
+                                    bool p_forcedBackground)
+{
+    if (p_timeStamp != m_codeBlockTimeStamp) {
+        return;
+    }
+
+    Q_UNUSED(p_format);
+    Q_ASSERT(p_format == QStringLiteral("svg"));
+
+    if (p_id >= static_cast<quint64>(m_codeBlocksData.size()) || p_data.isEmpty()) {
+        updateEditorInplacePreviewCodeBlock();
+        return;
+    }
+
+    auto &blockData = m_codeBlocksData[p_id];
+    auto previewData = QSharedPointer<GraphPreviewData>::create(p_timeStamp,
+                                                                p_format,
+                                                                p_data.toUtf8(),
+                                                                p_forcedBackground ? m_editor->getPreviewBackground() : 0,
+                                                                getEditorScaleFactor());
+    m_codeBlockCache.set(blockData.m_text, previewData);
+    blockData.m_text.clear();
+
+    blockData.updateInplacePreview(m_document,
+                                   previewData->m_image,
+                                   previewData->m_name,
+                                   previewData->m_background,
+                                   m_tabStopWidth);
+
+    updateEditorInplacePreviewCodeBlock();
+}
+
+bool PreviewHelper::needForcedBackground(const QString &p_lang) const
+{
+    if (checkPreviewSourceLang(SourceFlag::PlantUml, p_lang)) {
+        return true;
+    }
+
+    return false;
+}

+ 16 - 0
src/widgets/editors/previewhelper.h

@@ -47,6 +47,10 @@ namespace vnotex
 
         void setMarkdownEditor(MarkdownEditor *p_editor);
 
+        void setWebPlantUmlEnabled(bool p_enabled);
+
+        void setWebGraphvizEnabled(bool p_enabled);
+
     public slots:
         void codeBlocksUpdated(vte::TimeStamp p_timeStamp,
                                const QVector<vte::peg::FencedCodeBlock> &p_codeBlocks);
@@ -180,8 +184,16 @@ namespace vnotex
 
         void updateEditorInplacePreviewMathBlock();
 
+        void handleLocalData(quint64 p_id,
+                             TimeStamp p_timeStamp,
+                             const QString &p_format,
+                             const QString &p_data,
+                             bool p_forcedBackground);
+
         qreal getEditorScaleFactor() const;
 
+        bool needForcedBackground(const QString &p_lang) const;
+
         MarkdownEditor *m_editor = nullptr;
 
         QTextDocument *m_document = nullptr;
@@ -213,6 +225,10 @@ namespace vnotex
         vte::LruCache<QString, QSharedPointer<GraphPreviewData>> m_codeBlockCache;
 
         vte::LruCache<QString, QSharedPointer<GraphPreviewData>> m_mathBlockCache;
+
+        bool m_webPlantUmlEnabled = true;
+
+        bool m_webGraphvizEnabled = true;
     };
 }
 

+ 1 - 0
src/widgets/locationinputwithbrowsebutton.cpp

@@ -12,6 +12,7 @@ LocationInputWithBrowseButton::LocationInputWithBrowseButton(QWidget *p_parent)
     : QWidget(p_parent)
 {
     auto layout = new QHBoxLayout(this);
+    layout->setContentsMargins(0, 0, 0, 0);
 
     m_lineEdit = WidgetsFactory::createLineEdit(this);
     layout->addWidget(m_lineEdit, 1);

+ 30 - 5
src/widgets/markdownviewwindow.cpp

@@ -29,6 +29,8 @@
 #include "toolbarhelper.h"
 #include "findandreplacewidget.h"
 #include "editors/statuswidget.h"
+#include "editors/plantumlhelper.h"
+#include "editors/graphvizhelper.h"
 
 using namespace vnotex;
 
@@ -40,7 +42,7 @@ MarkdownViewWindow::MarkdownViewWindow(QWidget *p_parent)
 
     setupUI();
 
-    m_previewHelper = new PreviewHelper(nullptr, this);
+    setupPreviewHelper();
 }
 
 MarkdownViewWindow::~MarkdownViewWindow()
@@ -176,7 +178,12 @@ void MarkdownViewWindow::handleEditorConfigChange()
 
     if (markdownEditorConfig.revision() != m_markdownEditorConfigRevision) {
         m_markdownEditorConfigRevision = markdownEditorConfig.revision();
+
+        m_previewHelper->setWebPlantUmlEnabled(markdownEditorConfig.getWebPlantUml());
+        m_previewHelper->setWebGraphvizEnabled(markdownEditorConfig.getWebGraphviz());
+
         HtmlTemplateHelper::updateMarkdownViewerTemplate(markdownEditorConfig);
+
         if (m_editor) {
             auto config = createMarkdownEditorConfig(markdownEditorConfig);
             m_editor->setConfig(config);
@@ -236,7 +243,7 @@ void MarkdownViewWindow::handleBufferChangedInternal(const QSharedPointer<FileOp
 
     TextViewWindowHelper::handleBufferChanged(this);
 
-    handleFileOpenParameters(p_paras);
+    handleFileOpenParameters(p_paras, false);
 }
 
 void MarkdownViewWindow::setupToolBar()
@@ -890,7 +897,7 @@ void MarkdownViewWindow::handleFindAndReplaceWidgetOpened()
     m_findAndReplace->setReplaceEnabled(!isReadMode());
 }
 
-void MarkdownViewWindow::handleFileOpenParameters(const QSharedPointer<FileOpenParameters> &p_paras)
+void MarkdownViewWindow::handleFileOpenParameters(const QSharedPointer<FileOpenParameters> &p_paras, bool p_twice)
 {
     if (!p_paras) {
         return;
@@ -905,7 +912,9 @@ void MarkdownViewWindow::handleFileOpenParameters(const QSharedPointer<FileOpenP
             m_editor->insertText(title);
         }
     } else {
-        setMode(p_paras->m_mode);
+        if (!p_twice || p_paras->m_forceMode) {
+            setMode(p_paras->m_mode);
+        }
 
         scrollToLine(p_paras->m_lineNumber);
     }
@@ -934,7 +943,7 @@ bool MarkdownViewWindow::isReadMode() const
 void MarkdownViewWindow::openTwice(const QSharedPointer<FileOpenParameters> &p_paras)
 {
     Q_ASSERT(!p_paras || !p_paras->m_newFile);
-    handleFileOpenParameters(p_paras);
+    handleFileOpenParameters(p_paras, true);
 }
 
 ViewWindowSession MarkdownViewWindow::saveSession() const
@@ -946,3 +955,19 @@ ViewWindowSession MarkdownViewWindow::saveSession() const
     }
     return session;
 }
+
+void MarkdownViewWindow::setupPreviewHelper()
+{
+    Q_ASSERT(!m_previewHelper);
+
+    m_previewHelper = new PreviewHelper(nullptr, this);
+
+    const auto &markdownEditorConfig = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig();
+    m_previewHelper->setWebPlantUmlEnabled(markdownEditorConfig.getWebPlantUml());
+    m_previewHelper->setWebGraphvizEnabled(markdownEditorConfig.getWebGraphviz());
+
+    PlantUmlHelper::getInst().init(markdownEditorConfig.getPlantUmlJar(),
+                                   markdownEditorConfig.getGraphvizExe(),
+                                   markdownEditorConfig.getPlantUmlCommand());
+    GraphvizHelper::getInst().init(markdownEditorConfig.getGraphvizExe());
+}

+ 3 - 1
src/widgets/markdownviewwindow.h

@@ -97,6 +97,8 @@ namespace vnotex
 
         void setupViewer();
 
+        void setupPreviewHelper();
+
         void syncTextEditorFromBuffer(bool p_syncPositionFromReadMode);
 
         void syncViewerFromBuffer(bool p_syncPositionFromEditMode);
@@ -131,7 +133,7 @@ namespace vnotex
 
         void setModeInternal(ViewWindowMode p_mode, bool p_syncBuffer);
 
-        void handleFileOpenParameters(const QSharedPointer<FileOpenParameters> &p_paras);
+        void handleFileOpenParameters(const QSharedPointer<FileOpenParameters> &p_paras, bool p_twice);
 
         void scrollToLine(int p_lineNumber);
 

+ 10 - 0
src/widgets/viewarea.cpp

@@ -22,10 +22,14 @@
 #include <core/vnotex.h>
 #include <core/configmgr.h>
 #include <core/coreconfig.h>
+#include <core/editorconfig.h>
+#include <core/markdowneditorconfig.h>
 #include <core/sessionconfig.h>
 #include <core/fileopenparameters.h>
 #include <notebook/node.h>
 #include <notebook/notebook.h>
+#include "editors/plantumlhelper.h"
+#include "editors/graphvizhelper.h"
 
 using namespace vnotex;
 
@@ -87,6 +91,12 @@ ViewArea::ViewArea(QWidget *p_parent)
                     p_win->handleEditorConfigChange();
                     return true;
                 });
+
+                const auto &markdownEditorConfig = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig();
+                PlantUmlHelper::getInst().update(markdownEditorConfig.getPlantUmlJar(),
+                                                 markdownEditorConfig.getGraphvizExe(),
+                                                 markdownEditorConfig.getPlantUmlCommand());
+                GraphvizHelper::getInst().update(markdownEditorConfig.getGraphvizExe());
             });
 
     m_fileCheckTimer = new QTimer(this);

+ 6 - 0
src/widgets/widgets.pri

@@ -28,11 +28,14 @@ SOURCES += \
     $$PWD/dialogs/tableinsertdialog.cpp \
     $$PWD/dragdropareaindicator.cpp \
     $$PWD/editors/editormarkdownvieweradapter.cpp \
+    $$PWD/editors/graphhelper.cpp \
+    $$PWD/editors/graphvizhelper.cpp \
     $$PWD/editors/markdowneditor.cpp \
     $$PWD/editors/markdowntable.cpp \
     $$PWD/editors/markdowntablehelper.cpp \
     $$PWD/editors/markdownviewer.cpp \
     $$PWD/editors/markdownvieweradapter.cpp \
+    $$PWD/editors/plantumlhelper.cpp \
     $$PWD/editors/previewhelper.cpp \
     $$PWD/editors/statuswidget.cpp \
     $$PWD/editors/texteditor.cpp \
@@ -123,11 +126,14 @@ HEADERS += \
     $$PWD/dialogs/tableinsertdialog.h \
     $$PWD/dragdropareaindicator.h \
     $$PWD/editors/editormarkdownvieweradapter.h \
+    $$PWD/editors/graphhelper.h \
+    $$PWD/editors/graphvizhelper.h \
     $$PWD/editors/markdowneditor.h \
     $$PWD/editors/markdowntable.h \
     $$PWD/editors/markdowntablehelper.h \
     $$PWD/editors/markdownviewer.h \
     $$PWD/editors/markdownvieweradapter.h \
+    $$PWD/editors/plantumlhelper.h \
     $$PWD/editors/previewhelper.h \
     $$PWD/editors/statuswidget.h \
     $$PWD/editors/texteditor.h \