diff --git a/docs/userGuide/puml.md b/docs/userGuide/puml.md
index d46a132c3f..99c44a5e70 100644
--- a/docs/userGuide/puml.md
+++ b/docs/userGuide/puml.md
@@ -34,12 +34,15 @@ Name | Type | Default | Description
--- | --- | --- | ---
alt | `string` | | **This must be specified.**
The alternative text of the diagram.
height | `string` | | The height of the diagram in pixels.
-src | `string` | | **This must be specified.**
The URL of the diagram.
The URL can be specified as absolute or relative references. More info in: _[Intra-Site Links]({{baseUrl}}/userGuide/formattingContents.html#intraSiteLinks)_
+src | `string` | |
The URL of the diagram if your PUML input is in another file.
The URL can be specified as absolute or relative references. More info in: _[Intra-Site Links]({{baseUrl}}/userGuide/formattingContents.html#intraSiteLinks)_
+name | `string` | | The name of the output file.
width | `string` | | The width of the diagram in pixels.
If both width and height are specified, width takes priority over height. It is to maintain the diagram's aspect ratio.
### Example
+You could have your PUML be written in a separate file or inline.
+
@@ -57,17 +60,33 @@ return success
@enduml
```
-_Markbind page_:
+_Markbind page separate file_:
```
```
+_Markbind page inline_:
+`````
+
+
+```puml
+@startuml
+alice -> bob ++ : hello
+bob -> bob ++ : self call
+bob -> bib ++ #005500 : hello
+bob -> george ** : create
+return done
+return rc
+bob -> george !! : delete
+return success
+@enduml
+```
+`````
-
diff --git a/package-lock.json b/package-lock.json
index 9bc479320c..1917a44816 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1933,6 +1933,11 @@
"which": "^1.2.9"
}
},
+ "crypto-js": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz",
+ "integrity": "sha1-cV8HC/YBTyrpkqmLOSkli3E/CNU="
+ },
"css-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
@@ -3226,8 +3231,7 @@
},
"ansi-regex": {
"version": "2.1.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"aproba": {
"version": "1.2.0",
@@ -3245,13 +3249,11 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -3264,18 +3266,15 @@
},
"code-point-at": {
"version": "1.1.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"concat-map": {
"version": "0.0.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"console-control-strings": {
"version": "1.1.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"core-util-is": {
"version": "1.0.2",
@@ -3378,8 +3377,7 @@
},
"inherits": {
"version": "2.0.3",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"ini": {
"version": "1.3.5",
@@ -3389,7 +3387,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -3402,20 +3399,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -3432,7 +3426,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -3505,8 +3498,7 @@
},
"number-is-nan": {
"version": "1.0.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"object-assign": {
"version": "4.1.1",
@@ -3516,7 +3508,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -3592,8 +3583,7 @@
},
"safe-buffer": {
"version": "5.1.2",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -3623,7 +3613,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -3641,7 +3630,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -3680,13 +3668,11 @@
},
"wrappy": {
"version": "1.0.2",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"yallist": {
"version": "3.0.3",
- "bundled": true,
- "optional": true
+ "bundled": true
}
}
},
diff --git a/package.json b/package.json
index fb90a2a33c..26764464d9 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"cheerio": "^0.22.0",
"chokidar": "^3.3.0",
"commander": "^3.0.2",
+ "crypto-js": "^3.1.0",
"fastmatter": "^2.1.1",
"figlet": "^1.2.4",
"find-up": "^4.1.0",
diff --git a/src/Page.js b/src/Page.js
index 7dc91c9a70..23e4b75aef 100644
--- a/src/Page.js
+++ b/src/Page.js
@@ -761,6 +761,7 @@ class Page {
rootPath: this.rootPath,
userDefinedVariablesMap: this.userDefinedVariablesMap,
headerIdMap: this.headerIdMap,
+ resultPath: this.resultPath,
};
return new Promise((resolve, reject) => {
markbinder.includeFile(this.sourcePath, fileConfig)
diff --git a/src/lib/markbind/src/constants.js b/src/lib/markbind/src/constants.js
index 758777b634..203ec89ff9 100644
--- a/src/lib/markbind/src/constants.js
+++ b/src/lib/markbind/src/constants.js
@@ -2,6 +2,7 @@ module.exports = {
// src/lib/markbind/src/parser.js
ATTRIB_INCLUDE_PATH: 'include-path',
ATTRIB_CWF: 'cwf',
+ ATTRIB_RESULT_PATH: 'resultPath',
BOILERPLATE_FOLDER_NAME: '_markbind/boilerplates',
diff --git a/src/lib/markbind/src/lib/markdown-it/index.js b/src/lib/markbind/src/lib/markdown-it/index.js
index 4d7c4d7cf4..75875c6d30 100644
--- a/src/lib/markbind/src/lib/markdown-it/index.js
+++ b/src/lib/markbind/src/lib/markdown-it/index.js
@@ -42,6 +42,31 @@ markdownIt.renderer.rules.table_close = (tokens, idx) => {
return '';
};
+// rewritten markdown-it renderer.js rule to add a div containing the language of the code block
+markdownIt.renderer.rules.fence = (tokens, idx, options, env, slf) => {
+ var token = tokens[idx],
+ info = token.info ? markdownIt.utils.unescapeAll(token.info).trim() : '',
+ langName = '',
+ highlighted;
+
+ if (info) {
+ langName = info.split(/\s+/g)[0];
+ }
+
+ const widgetHandler = env[langName];
+ if (widgetHandler !== undefined) {
+ return widgetHandler(token.content);
+ }
+
+ if (options.highlight) {
+ highlighted = options.highlight(token.content, langName) || escapeHtml(token.content);
+ } else {
+ highlighted = escapeHtml(token.content);
+ }
+
+ return highlighted + '\n';
+}
+
// highlight inline code
markdownIt.renderer.rules.code_inline = (tokens, idx, options, env, slf) => {
const token = tokens[idx];
diff --git a/src/lib/markbind/src/parser.js b/src/lib/markbind/src/parser.js
index 7a8fc3a11c..b9426d2b79 100644
--- a/src/lib/markbind/src/parser.js
+++ b/src/lib/markbind/src/parser.js
@@ -8,6 +8,7 @@ const url = require('url');
const pathIsInside = require('path-is-inside');
const slugify = require('@sindresorhus/slugify');
const componentParser = require('./parsers/componentParser');
+const widget = require('../../../widgets/widgets');
const _ = {};
_.clone = require('lodash/clone');
@@ -383,6 +384,10 @@ class Parser {
element.children = element.children.map(e => self._preprocess(e, context, config));
}
}
+
+
+ widget.preprocessWidget(config, context, element);
+
return element;
}
@@ -512,6 +517,8 @@ class Parser {
break;
}
+ widget.parseWidget(config, context, element);
+
componentParser.parseComponents(element, this._onError);
if (element.children) {
@@ -568,6 +575,7 @@ class Parser {
}
return processed;
});
+
resolve(cheerio.html(nodes));
});
diff --git a/src/lib/markbind/src/utils.js b/src/lib/markbind/src/utils.js
index 4e6eee399f..619dc8c2cb 100644
--- a/src/lib/markbind/src/utils.js
+++ b/src/lib/markbind/src/utils.js
@@ -1,5 +1,6 @@
const fs = require('fs');
const path = require('path');
+const cheerio = require('cheerio');
const { markdownFileExts } = require('./constants');
@@ -100,4 +101,21 @@ module.exports = {
return text.join('').trim();
},
+
+ extractCodeElement(element, codeLanguage = '') {
+ let codeBlockElement = cheerio.parseHTML('', true)[0];
+
+ element.children.forEach((child) => {
+ if (child.name === 'pre' && child.children.length >= 1) {
+ const [nestedChild] = child.children;
+ if (nestedChild.name !== 'code') {
+ return;
+ }
+ [codeBlockElement] = nestedChild.children;
+ codeBlockElement.data = `\`\`\`${codeLanguage}\n${codeBlockElement.data}\n\`\`\``;
+ }
+ });
+
+ return codeBlockElement;
+ },
};
diff --git a/src/plugins/default/markbind-plugin-plantuml.js b/src/plugins/default/markbind-plugin-plantuml.js
deleted file mode 100644
index a7d87b7a1f..0000000000
--- a/src/plugins/default/markbind-plugin-plantuml.js
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * Parses PlantUML diagrams
- * Replaces tags with tags with the appropriate src attribute and generates the diagrams
- * by running the JAR executable
- */
-const cheerio = module.parent.require('cheerio');
-const fs = require('fs');
-const path = require('path');
-const { exec } = require('child_process');
-const logger = require('../../util/logger');
-
-const JAR_PATH = path.resolve(__dirname, 'plantuml.jar');
-
-const {
- ERR_PROCESSING,
- ERR_READING,
-} = require('../../constants');
-
-// Tracks diagrams that have already been processed
-const processedDiagrams = new Set();
-
-/**
- * Generates diagram and replaces src attribute of puml tag
- * @param src src attribute of the puml tag
- * @param cwf original file that contains the puml tag, we resolve relative src to this file
- * @param config sourcePath and resultPath from parser context
- * @returns {string} resolved src attribute
- */
-function generateDiagram(src, cwf, config) {
- const { sourcePath, resultPath } = config;
- const _cwf = cwf || sourcePath;
-
- // For replacing img.src
- const diagramSrc = src.replace('.puml', '.png');
- // Path of the .puml file
- const rawDiagramPath = path.resolve(path.dirname(_cwf), src);
- // Path of the .png to be generated
- const outputFilePath = path.resolve(resultPath, path.relative(sourcePath, rawDiagramPath)
- .replace('.puml', '.png'));
- // Output dir for the png file
- const outputDir = path.dirname(outputFilePath);
-
- // Tracks built files to avoid accessing twice
- if (processedDiagrams.has(outputFilePath)) { return diagramSrc; }
- processedDiagrams.add(outputFilePath);
-
- // Creates output dir if it doesn't exist
- if (!fs.existsSync(outputDir)) {
- fs.mkdirSync(outputDir, { recursive: true });
- }
-
- // Read diagram file, launch PlantUML jar
- fs.readFile(rawDiagramPath, (err, data) => {
- if (err) {
- logger.debug(err);
- logger.error(`${ERR_READING} ${rawDiagramPath}`);
- return;
- }
-
- const umlCode = data.toString();
-
- // Java command to launch PlantUML jar
- const cmd = `java -jar "${JAR_PATH}" -pipe > "${outputFilePath}"`;
- const childProcess = exec(cmd);
-
- let errorLog = '';
-
- childProcess.stdin.write(
- umlCode,
- (e) => {
- if (e) {
- logger.debug(e);
- logger.error(`${ERR_PROCESSING} ${rawDiagramPath}`);
- }
- childProcess.stdin.end();
- },
- );
-
- childProcess.on('error', (error) => {
- logger.debug(error);
- logger.error(`${ERR_PROCESSING} ${rawDiagramPath}`);
- });
-
- childProcess.stderr.on('data', (errorMsg) => {
- errorLog += errorMsg;
- });
-
- childProcess.on('exit', () => {
- // This goes to the log file, but not shown on the console
- logger.debug(errorLog);
- });
- });
-
- return diagramSrc;
-}
-
-module.exports = {
- preRender: (content, pluginContext, frontmatter, config) => {
- // Clear tags processed before for live reload
- processedDiagrams.clear();
- // Processes all tags
- const $ = cheerio.load(content, { xmlMode: true });
- $('puml').each((i, tag) => {
- // eslint-disable-next-line no-param-reassign
- tag.name = 'pic';
- const { src, cwf } = tag.attribs;
- // eslint-disable-next-line no-param-reassign
- tag.attribs.src = generateDiagram(src, cwf, config);
- });
-
- return $.html();
- },
- getSources: () => ({
- tagMap: [['puml', 'src']],
- }),
-};
diff --git a/src/plugins/default/plantuml.jar b/src/widgets/plantuml.jar
similarity index 100%
rename from src/plugins/default/plantuml.jar
rename to src/widgets/plantuml.jar
diff --git a/src/widgets/puml.js b/src/widgets/puml.js
new file mode 100644
index 0000000000..aa461ab018
--- /dev/null
+++ b/src/widgets/puml.js
@@ -0,0 +1,159 @@
+const path = require('path');
+const cheerio = require('cheerio');
+const cryptoJS = require('crypto-js');
+const fs = require('fs');
+const { exec } = require('child_process');
+
+const parserUtil = require('../lib/markbind/src/utils');
+const logger = require('../util/logger');
+const fsUtil = require('../util/fsUtil');
+const md = require('../lib/markbind/src/lib/markdown-it');
+const {
+ ATTRIB_RESULT_PATH,
+} = require('../lib/markbind/src/constants');
+
+const _ = {};
+_.hasIn = require('lodash/hasIn');
+
+const PLANTUML_JAR_NAME = 'plantuml.jar';
+const PUML_NAME = 'puml';
+
+function getFileName(context, content) {
+ if (context.name !== undefined) {
+ return `${context.name}.png`;
+ }
+
+ if (context.src !== undefined) {
+ const fileName = fsUtil.removeExtension(context.src);
+ return `${fileName}.png`;
+ }
+
+ const hashedContent = cryptoJS.MD5(content).toString();
+ return `${hashedContent}.png`;
+}
+
+function generateElement(context) {
+ const element = cheerio.parseHTML('')[0];
+ const entries = Object.entries(context);
+ entries.forEach(([key, val]) => {
+ if (key === ATTRIB_RESULT_PATH) {
+ return;
+ }
+ element.attribs[key] = val;
+ });
+
+ return element;
+}
+
+/*
+* Curry function takes in context returning a widgetHandler function
+* PUML widgetHandler takes in raw PUML contents and returns img with puml
+*/
+function parseAndRender(context) {
+ return function (content) {
+ const outputDir = path.dirname(context.resultPath);
+ const fileName = getFileName(context, content);
+ // Path of the .puml file
+ const outputPath = path.join(outputDir, path.basename(fileName));
+ const JAR_PATH = path.resolve(__dirname, PLANTUML_JAR_NAME);
+
+ // Creates output dir if it doesn't exist
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ // Java command to launch PlantUML jar
+ const cmd = `java -jar "${JAR_PATH}" -pipe > "${outputPath}"`;
+ const childProcess = exec(cmd);
+
+ let errorLog = '';
+ childProcess.stdin.write(
+ content,
+ (e) => {
+ if (e) {
+ logger.debug(e);
+ }
+ childProcess.stdin.end();
+ },
+ );
+
+ childProcess.on('error', (error) => {
+ logger.debug(error);
+ });
+
+ childProcess.stderr.on('data', (errorMsg) => {
+ errorLog += errorMsg;
+ });
+
+ childProcess.on('exit', () => {
+ // This goes to the log file, but not shown on the console
+ logger.debug(errorLog);
+ });
+
+ const elementContext = context;
+ elementContext.src = fileName;
+ const element = generateElement(elementContext);
+
+ return cheerio.html(element);
+ };
+}
+
+class Puml {
+ constructor() {
+ this.name = PUML_NAME;
+ }
+
+ preprocess(config, context, pumlElement) { // eslint-disable-line class-methods-use-this
+ const element = pumlElement;
+ if (!_.hasIn(element.attribs, 'src')) {
+ return element;
+ }
+ const resultPath = path.resolve(path.dirname(config.resultPath), element.attribs.src);
+ const pumlContext = {
+ resultPath,
+ ...element.attribs,
+ };
+
+ // Path of the .puml file
+ const rawDiagramPath = path.resolve(path.dirname(context.cwf), pumlContext.src);
+ const pumlContent = fs.readFileSync(rawDiagramPath, 'utf8');
+ const generatePUML = parseAndRender(pumlContext);
+
+ const childrenDOM = generatePUML(pumlContent);
+ element.name = 'div';
+ element.attribs = {};
+
+ element.children = cheerio.parseHTML(childrenDOM);
+ element.children.cwf = context.cwf;
+ return element;
+ }
+
+ parse(config, context, pumlElement) {
+ const element = pumlElement;
+
+ const pumlContext = {
+ resultPath: config.resultPath,
+ ...element.attribs,
+ };
+
+ element.name = 'div';
+ element.attribs = {};
+ cheerio.prototype.options.xmlMode = false;
+ const generatePUML = parseAndRender(pumlContext);
+ const widgetHandler = {};
+ widgetHandler[this.name] = generatePUML;
+
+ const pumlContent = parserUtil.extractCodeElement(element, this.name);
+
+ element.children = cheerio.parseHTML(md.render(cheerio.html(pumlContent), widgetHandler), true);
+ return element;
+ }
+}
+
+/*
+* Parse and render PUML widget
+*/
+module.exports = {
+ parseAndRender,
+ Puml,
+};
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
new file mode 100644
index 0000000000..801d48ec22
--- /dev/null
+++ b/src/widgets/widgets.js
@@ -0,0 +1,28 @@
+const pumlWidget = require('./puml');
+
+const WIDGETS = {
+ puml: new pumlWidget.Puml(),
+};
+
+function preprocessWidget(config, context, element) {
+ const widget = WIDGETS[element.name];
+ if (widget === undefined) {
+ return element;
+ }
+
+ return widget.preprocess(config, context, element);
+}
+
+function parseWidget(config, context, element) {
+ const widget = WIDGETS[element.name];
+ if (widget === undefined) {
+ return element;
+ }
+
+ return widget.parse(config, context, element);
+}
+
+module.exports = {
+ preprocessWidget,
+ parseWidget,
+};
diff --git a/test/functional/test_site/expected/b169e11db4f52a01eb352071b156c59b.png b/test/functional/test_site/expected/b169e11db4f52a01eb352071b156c59b.png
new file mode 100644
index 0000000000..e6e95dbbe9
Binary files /dev/null and b/test/functional/test_site/expected/b169e11db4f52a01eb352071b156c59b.png differ
diff --git a/test/functional/test_site/expected/index.html b/test/functional/test_site/expected/index.html
index dc18a1dd28..b9598c2667 100644
--- a/test/functional/test_site/expected/index.html
+++ b/test/functional/test_site/expected/index.html
@@ -469,33 +469,48 @@ Markbind Plugin Pre-render
-
+
Test PlantUML live reload with include
PlantUML Test
-
Sequence Diagram
+
Sequence Diagram
+
+
Use Case Diagram
+
+
Class Diagram
+
+
Activity Diagram
+
+
Component Diagram
+
+
State Diagram
+
+
Object Diagram
+
+
PlantUML Inline Test
+
+
diff --git a/test/functional/test_site/expected/inline-output.png b/test/functional/test_site/expected/inline-output.png
new file mode 100644
index 0000000000..32a71b61dc
Binary files /dev/null and b/test/functional/test_site/expected/inline-output.png differ
diff --git a/test/functional/test_site/expected/testPlantUML.html b/test/functional/test_site/expected/testPlantUML.html
index f766091d47..859cb60bfd 100644
--- a/test/functional/test_site/expected/testPlantUML.html
+++ b/test/functional/test_site/expected/testPlantUML.html
@@ -26,27 +26,41 @@
PlantUML Test
-
Sequence Diagram
+
Sequence Diagram
+
+
Use Case Diagram
+
+
Class Diagram
+
+
Activity Diagram
+
+
Component Diagram
+
+
State Diagram
+
+
Object Diagram
+
+
PlantUML Inline Test
+
+