diff --git a/Gruntfile.js b/Gruntfile.js index 4f4b3b027..dcb96a2c0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -151,6 +151,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/font-awesome.js", "packages/node_modules/@node-red/editor-client/src/js/history.js", "packages/node_modules/@node-red/editor-client/src/js/validators.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/mermaid.js", "packages/node_modules/@node-red/editor-client/src/js/ui/utils.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js", @@ -225,7 +226,7 @@ module.exports = function(grunt) { "node_modules/jsonata/jsonata-es5.min.js", "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/formatter.js", "packages/node_modules/@node-red/editor-client/src/vendor/ace/ace.js", - "packages/node_modules/@node-red/editor-client/src/vendor/ace/ext-language_tools.js", + "packages/node_modules/@node-red/editor-client/src/vendor/ace/ext-language_tools.js" ], // "packages/node_modules/@node-red/editor-client/public/vendor/vendor.css": [ // // TODO: resolve relative resource paths in @@ -234,6 +235,9 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/public/vendor/ace/worker-jsonata.js": [ "node_modules/jsonata/jsonata-es5.min.js", "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/worker-jsonata.js" + ], + "packages/node_modules/@node-red/editor-client/public/vendor/mermaid/mermaid.min.js": [ + "node_modules/mermaid/dist/mermaid.min.js" ] } } diff --git a/package.json b/package.json index ad65edd90..ed0edd2b0 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "jquery-i18next": "1.2.1", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template", "marked": "4.2.3", + "mermaid": "^9.3.0", "minami": "1.2.3", "mocha": "9.2.2", "node-red-node-test-helper": "^0.3.0", diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/editor-libs.js b/packages/node_modules/@node-red/editor-api/lib/editor/editor-libs.js new file mode 100644 index 000000000..98391dc12 --- /dev/null +++ b/packages/node_modules/@node-red/editor-api/lib/editor/editor-libs.js @@ -0,0 +1,33 @@ +// Dynamic loading support for editor libraries + +var apiUtils = require("../util"); +var fs = require("fs"); +var path = require("path"); + +var lib2path = { + "mermaid": "../../../editor-client/public/vendor/mermaid/mermaid.min.js", +}; + +module.exports = { + init: function(_settings, _runtimeAPI) { + settings = _settings; + }, + + get: function(req,res) { + var name = req.params.name; + + if (name in lib2path) { + try { + var lib = path.join(__dirname, lib2path[name]); + var code = fs.readFileSync(lib); + res.send(code); + } + catch (e) { + res.status(500).json({code: "runtime_error", message: e.toString()}); + } + } + else { + res.status(400).json({code: "invalid_request", message: `no library: ${name}`}); + } + }, +} diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/index.js b/packages/node_modules/@node-red/editor-api/lib/editor/index.js index f210d90fe..66d088f3a 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/index.js @@ -116,6 +116,11 @@ module.exports = { // SSH keys editorApp.use("/settings/user/keys",needsPermission("settings.write"),info.sshkeys()); + // Editor Libraries + var editorLibs = require("./editor-libs"); + editorLibs.init(settings, runtimeAPI); + editorApp.get("/editor-libs/:name", needsPermission("editor-libs.read"), editorLibs.get, apiUtil.errorHandler); + return editorApp; } }, diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 9ffde4df0..e768c1cb3 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -987,7 +987,10 @@ "quote": "Quote", "link": "Link", "horizontal-rule": "Horizontal rule", - "toggle-preview": "Toggle preview" + "toggle-preview": "Toggle preview", + "mermaid": { + "summary": "Mermaid Diagram" + } }, "bufferEditor": { "title": "Buffer editor", diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index 30761ef14..fcdf35e3d 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -987,7 +987,10 @@ "quote": "引用", "link": "リンク", "horizontal-rule": "区切り線", - "toggle-preview": "プレビュー表示切替え" + "toggle-preview": "プレビュー表示切替え", + "mermaid": { + "summary": "Mermaid図" + } }, "bufferEditor": { "title": "バッファエディタ", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/markdown.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/markdown.js index eeb8519e6..93e34e126 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/markdown.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/markdown.js @@ -114,6 +114,7 @@ var currentScrollTop = $(".red-ui-editor-type-markdown-panel-preview").scrollTop(); $(".red-ui-editor-type-markdown-panel-preview").html(RED.utils.renderMarkdown(expressionEditor.getValue())); $(".red-ui-editor-type-markdown-panel-preview").scrollTop(currentScrollTop); + mermaid.init(); },200); }) if (options.header) { @@ -122,6 +123,7 @@ if (value) { $(".red-ui-editor-type-markdown-panel-preview").html(RED.utils.renderMarkdown(expressionEditor.getValue())); + mermaid.init(); } panels = RED.panels.create({ id:"red-ui-editor-type-markdown-panels", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/mermaid.js b/packages/node_modules/@node-red/editor-client/src/js/ui/mermaid.js new file mode 100644 index 000000000..f16aae5a0 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/mermaid.js @@ -0,0 +1,46 @@ +// Mermaid diagram stub library for on-demand dynamic loading +// Will be overwritten after script loading by $.getScript +var mermaid = (function () { + var enabled /* = undefined */; + + var initializing = false; + var initCalled = false; + + function initialize(opt) { + if (enabled === undefined) { + if (RED.settings.markdownEditor && + RED.settings.markdownEditor.mermaid) { + enabled = RED.settings.markdownEditor.mermaid.enabled; + } + else { + enabled = true; + } + } + if (enabled) { + initializing = true; + $.getScript("/editor-libs/mermaid", + function (data, stat, jqxhr) { + $(".mermaid").show(); + // invoke loaded mermaid API + initializing = false; + mermaid.initialize(opt); + if (initCalled) { + mermaid.init(); + initCalled = false; + } + }); + } + } + + function init() { + if (initializing) { + $(".mermaid").hide(); + initCalled = true; + } + } + + return { + initialize: initialize, + init: init, + }; +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js index c46aa97e8..07818ffd2 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js @@ -463,7 +463,8 @@ RED.sidebar.info = (function() { el = el.next(); } $(this).toggleClass('expanded',!isExpanded); - }) + }); + mermaid.init(); } var tips = (function() { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index 2c4cdca6b..4d8ccdd9d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -96,6 +96,37 @@ RED.utils = (function() { } } + var mermaidIsInitialized = false; + var mermaidIsEnabled /* = undefined */; + + renderer.code = function (code, lang) { + if(lang === "mermaid") { + // mermaid diagram rendering + if (mermaidIsEnabled === undefined) { + if (RED.settings.markdownEditor && + RED.settings.markdownEditor.mermaid) { + mermaidIsEnabled = RED.settings.markdownEditor.mermaid.enabled; + } + else { + mermaidIsEnabled = true; + } + } + if (mermaidIsEnabled) { + if (!mermaidIsInitialized) { + mermaidIsInitialized = true; + mermaid.initialize({startOnLoad:false}); + } + return `
${code}
`; + } + else { + return `
${RED._("markdownEditor.mermaid.summary")}
${code}
`; + } + } + else { + return "
" +code +"
"; + } + }; + window._marked.setOptions({ renderer: renderer, gfm: true, diff --git a/packages/node_modules/@node-red/runtime/lib/api/settings.js b/packages/node_modules/@node-red/runtime/lib/api/settings.js index 6c13596ce..2399a3152 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/settings.js +++ b/packages/node_modules/@node-red/runtime/lib/api/settings.js @@ -89,10 +89,16 @@ var api = module.exports = { if (!runtime.settings.disableEditor) { safeSettings.context = runtime.nodes.listContextStores(); - if (runtime.settings.editorTheme && runtime.settings.editorTheme.codeEditor) { - safeSettings.codeEditor = runtime.settings.editorTheme.codeEditor || {}; - safeSettings.codeEditor.lib = safeSettings.codeEditor.lib || "monaco"; - safeSettings.codeEditor.options = safeSettings.codeEditor.options || {}; + if (runtime.settings.editorTheme) { + if (runtime.settings.editorTheme.codeEditor) { + safeSettings.codeEditor = runtime.settings.editorTheme.codeEditor || {}; + safeSettings.codeEditor.lib = safeSettings.codeEditor.lib || "monaco"; + safeSettings.codeEditor.options = safeSettings.codeEditor.options || {}; + } + if (runtime.settings.editorTheme.markdownEditor) { + safeSettings.markdownEditor = runtime.settings.editorTheme.markdownEditor || {}; + safeSettings.markdownEditor.mermaid = safeSettings.markdownEditor.mermaid || { enabled: true }; + } } safeSettings.libraries = runtime.library.getLibraries(); if (util.isArray(runtime.settings.paletteCategories)) { diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index ec247a672..5d8a05e44 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -422,7 +422,16 @@ module.exports = { //fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace", //fontLigatures: true, } - } + }, + + markdownEditor: { + mermaid: { + /** enable or disable mermaid diagram in markdown document + */ + enabled: true + } + }, + }, /******************************************************************************* diff --git a/test/unit/@node-red/editor-api/lib/editor/editor-libs_spec.js b/test/unit/@node-red/editor-api/lib/editor/editor-libs_spec.js new file mode 100644 index 000000000..4a8158d74 --- /dev/null +++ b/test/unit/@node-red/editor-api/lib/editor/editor-libs_spec.js @@ -0,0 +1,49 @@ +const should = require("should"); +const request = require('supertest'); +const express = require('express'); +const bodyParser = require("body-parser"); +const sinon = require('sinon'); + +let app; + +const NR_TEST_UTILS = require("nr-test-utils"); +const editorLibs = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/editor-libs"); + +describe("api/editor/editor-libs", function() { + before(function() { + app = express(); + app.use(bodyParser.json()); + app.get("/editor-libs/:name", editorLibs.get); + }); + + it("returns the editor library for mermaid", function(done) { + const settings = {}; + const runtimeAPI = {}; + + editorLibs.init(settings, runtimeAPI); + + request(app) + .get("/editor-libs/mermaid") + .expect(200) + .end(function(err,res) { + if (err || (typeof res.error === "object")) { + return done(err || res.error); + } + res.should.have.property("statusCode",200); + res.should.have.property("_body"); + done(); + }); + }); + + it('should error when called with unknown library', function(done) { + const settings = {}; + const runtimeAPI = {}; + + editorLibs.init(settings, runtimeAPI); + + request(app) + .get("/editor-libs/unknown") + .expect(400) + .end(done); + }); +});