Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Mermaid.js in kaleido #177

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a2664b9
fix: build_kaleido linux script
coma007 Mar 27, 2024
6a8cefe
add: playground debug script
coma007 Mar 27, 2024
ab4e155
chore: .gitignore update
coma007 Mar 28, 2024
5fb7a62
feat: mermaid scope pipeline
coma007 Apr 3, 2024
b0e0a31
feat: mermaid.py library
coma007 Apr 4, 2024
4a3a1e1
perf: esm script tags
coma007 Apr 4, 2024
9d467d3
feat: Mermaid.h library
coma007 Apr 4, 2024
7e25bc5
feat: render.js for mermaid library
coma007 Apr 4, 2024
0409ca1
fix: syntax errors
coma007 Apr 4, 2024
089b49d
fix: mermaid object init in script
coma007 Apr 4, 2024
3d4741e
fix: rendering call error
coma007 Apr 4, 2024
8e7f184
refactor: semantical changes
coma007 Apr 5, 2024
7d55541
refactor: mermaidjs module kaleido build argument
coma007 Apr 5, 2024
1e55d59
refactor: isScriptModule function
coma007 Apr 5, 2024
fa53a2c
fix: regex checks
coma007 Apr 5, 2024
02d0d42
chore: remove image decoding
coma007 Apr 5, 2024
9fd5c2a
feat: mermaidConfig as cmd line arg
coma007 Apr 5, 2024
8d68bb9
fix: ends with string util
coma007 Apr 5, 2024
0ef6e7a
Merge pull request #1 from coma007/feat/mermaid-scope
coma007 Apr 5, 2024
5ad7e17
feat: parse request check & diagram config
coma007 Apr 9, 2024
7e0721d
feat: config in transform function
coma007 Apr 9, 2024
5b5c5f8
refactor: renaming and docs
coma007 Apr 10, 2024
1fcd659
refactor: config initialization
coma007 Apr 10, 2024
d39a7a3
refactor: js utils
coma007 Apr 10, 2024
1321a34
fix: json parsing
coma007 Apr 10, 2024
da5e349
fix: propperties function
coma007 Apr 10, 2024
3a5c2d2
style: formatting
coma007 Apr 10, 2024
435ff66
fix: loop break
coma007 Apr 10, 2024
ffe5e06
fix: removed mermaid config for init
coma007 Apr 11, 2024
15aaa67
Merge pull request #2 from coma007/feat/mermaid-validation
coma007 Apr 11, 2024
5343eb8
Merge pull request #3 from coma007/dev
coma007 Apr 11, 2024
3352383
clean: repo cleanup
coma007 Apr 11, 2024
c94f05c
docs: readme
coma007 Apr 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ repos/kaleido/py/dist/
repos/kaleido/py/scratch/
.idea/
repos/CREDITS.html
venv/*
repos/old_src_third_party*/*
repos/.gclient_previous_sync_commits
.vscode/*
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,32 @@ Then, open `figure.png` in the current working directory.

![figure](https://user-images.githubusercontent.com/15064365/86343448-f8f7f400-bc26-11ea-9191-6803748c2dc9.png)

# Use Kaleido to transform Mermaid.js markdown into static image

Bellow is an example of usage of kaleido to transform [Mermaid.js](https://mermaid.js.org/) markdown into image.

> Note: This particular example uses an online copy of the Mermaid JavaScript library from a CDN location, so it will not work without an internet connection.

```python
from kaleido.scopes.mermaid import MermaidScope

scope = MermaidScope()

graphDefinition = """
graph LR
A --- B
B-->C[fa:fa-ban forbidden]
B-->D(fa:fa-spinner);
"""

data = scope.transform(graphDefinition, format="svg", config={"theme" : "default"})
with open("figure.svg", "wb") as f:
f.write(data)
```
![figure](https://github.com/coma007/Kaleido/assets/76025555/3dccf1d9-e065-430d-a0d8-17f0437d866c)

Then, open `figure.svg` in the current working directory.


# Background
As simple as it sounds, programmatically generating static images (e.g. raster images like PNGs or vector images like SVGs) from web-based visualization libraries (e.g. Plotly.js, Vega-Lite, etc.) is a complex problem. It's a problem that library developers have struggled with for years, and it has delayed the adoption of these libraries among scientific communities that rely on print-based publications for sharing their research. The core difficulty is that web-based visualization libraries don't actually render plots (i.e. color the pixels) on their own, instead they delegate this work to web technologies like SVG, Canvas, WebGL, etc. Similar to how Matplotlib relies on various backends to display figures, web-based visualization libraries rely on a web browser rendering engine to display figures.
Expand Down
10 changes: 8 additions & 2 deletions repos/kaleido/cc/kaleido.cc
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -561,8 +561,14 @@ void OnHeadlessBrowserStarted(headless::HeadlessBrowser* browser) {
// Value is a url, use a src of script tag
htmlStringStream << "<script type=\"text/javascript\" src=\"" << tagValue << "\"></script>";
} else {
// Value is not a url, use a inline JavaScript code
htmlStringStream << "<script>" << tagValue << "</script>\n";
if (kaleido::utils::containsImportStatement(tagValue)) {
// Value is a module, use a type="module" script tag
htmlStringStream << "<script type=\"module\">" << tagValue << "</script>\n";
}
else {
// Value is not a url nor a module, use a inline JavaScript code
htmlStringStream << "<script>" << tagValue << "</script>\n";
}
}
scriptTags.pop_front();
}
Expand Down
3 changes: 3 additions & 0 deletions repos/kaleido/cc/scopes/Factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "base/strings/string_util.h"

#include "Mermaid.h"
#include "Plotly.h"
#include "Base.h"

Expand All @@ -14,6 +15,8 @@ kaleido::scopes::BaseScope* LoadScope(std::string name) {
std::string name_lower = base::ToLowerASCII(name);
if (name_lower == "plotly") {
return new kaleido::scopes::PlotlyScope();
} else if (name_lower == "mermaid") {
return new kaleido::scopes::MermaidScope();
} else {
return nullptr;
}
Expand Down
88 changes: 88 additions & 0 deletions repos/kaleido/cc/scopes/Mermaid.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// Created by coma007 on 4/4/24.
//
#include "Base.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "headless/public/devtools/domains/runtime.h"
#include "../utils.h"
#include <streambuf>
#include <string>
#include <sstream>
#include <iostream>
#include <fstream>

#ifndef CHROMIUM_MERMAIDSCOPE_H
#define CHROMIUM_MERMAIDSCOPE_H

namespace kaleido {
namespace scopes {

class MermaidScope : public BaseScope {
public:
MermaidScope();

~MermaidScope() override;

MermaidScope(const MermaidScope &v);

std::string ScopeName() override;

std::vector<std::unique_ptr<::headless::runtime::CallArgument>> BuildCallArguments() override;
};

MermaidScope::MermaidScope() {

// Initialize mermaid object code
std::string mermaidInit = "; window.mermaid = mermaid;";

// Process mermaidjs
if (HasCommandLineSwitch("mermaidjs")) {
std::string mermaidjsArg = GetCommandLineSwitch("mermaidjs");

// Check if value is a URL
GURL mermaidjsUrl(mermaidjsArg);
if (mermaidjsUrl.is_valid()) {
// ESM module
if (kaleido::utils::isESModule(mermaidjsArg)) {
scriptTags.push_back("import mermaid from '" + mermaidjsArg + "'" + mermaidInit);
}
// Default module
else {
scriptTags.push_back(mermaidjsArg + mermaidInit);
}
} else {
// Check if this is a local file path
if (std::ifstream(mermaidjsArg)) {
localScriptFiles.emplace_back(mermaidjsArg);
} else {
errorMessage = base::StringPrintf("--mermaidjs argument is not a valid URL or file path: %s",
mermaidjsArg.c_str());
return;
}
}
} else {
scriptTags.emplace_back("import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/+esm'" + mermaidInit);
}

}

MermaidScope::~MermaidScope() {}

MermaidScope::MermaidScope(const MermaidScope &v) {}

std::string MermaidScope::ScopeName() {
return "mermaid";
}

std::vector<std::unique_ptr<::headless::runtime::CallArgument>> MermaidScope::BuildCallArguments() {
std::vector<std::unique_ptr<::headless::runtime::CallArgument>> args;

return args;
}
}
}

#endif //CHROMIUM_MERMAIDSCOPE_H
17 changes: 17 additions & 0 deletions repos/kaleido/cc/utils.h
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <iostream>
#include <fstream>
#include <regex>

#ifndef CHROMIUM_UTILS_H
#define CHROMIUM_UTILS_H
Expand All @@ -21,6 +22,22 @@ namespace kaleido {
code, message.c_str(), version.c_str());
std::cout << error;
}

bool endsWith(const std::string& fullString, const std::string& ending)
{
if (ending.size() > fullString.size())
return false;
return fullString.compare(fullString.size() - ending.size(), ending.size(), ending) == 0;
}

bool containsImportStatement(std::string scriptTag) {
std::regex pattern(R"(\bimport\b)");
return std::regex_search(scriptTag, pattern);
}

bool isESModule(std::string scriptTag) {
return endsWith(scriptTag, "+esm");
}
}
}

Expand Down
1 change: 1 addition & 0 deletions repos/kaleido/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"fast-isnumeric": "^1.1.4",
"is-plain-obj": "^2.1.0",
"object.hasown": "^1.1.3",
"semver": "^7.3.2"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions repos/kaleido/js/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
name: 'kaleido_scopes',
plotly: require('./plotly/render'),
mermaid: require('./mermaid/render'),
// Additional plugins go here
}
18 changes: 18 additions & 0 deletions repos/kaleido/js/src/mermaid/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
contentFormat: {
svg: 'image/svg+xml',
},

statusMsg: {
400: 'invalid or malformed request',
406: 'requested format is not acceptable',
},

defaultParams: {
format: 'svg',
scale: 1,
width: 700,
height: 500
},
}

67 changes: 67 additions & 0 deletions repos/kaleido/js/src/mermaid/parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const constants = require('./constants')
const isPositiveNumeric = require('../utils/is-positive-numeric')
const isNonEmptyString = require('../utils/is-non-empty-string')
const hasPropertiesOfObject = require('../utils/has-properties-of-object')

/** mermaid-graph parse
*
* @param {object} body : JSON-parsed request body
* - data
* - format
* - scale
* - width
* - height
* - config
* @return {object}
* - errorCode
* - result
*/
function parse (body) {

let result = body;
result.code = 0;

const errorOut = (code, extra) => {
let message = `${constants.statusMsg[code]}`
if (extra) {
message = `${message} (${extra})`
}
return {code, message, result: null}
}

if (!isNonEmptyString(body.data)) {
return errorOut(400, 'empty markdown')
}

if (isNonEmptyString(body.format)) {
if (constants.contentFormat[body.format]) {
result.format = body.format
} else {
return errorOut(406, 'wrong format')
}
} else {
result.format = constants.defaultParams.format
}

result.scale = isPositiveNumeric(body.scale) ? Number(body.scale) : constants.defaultParams.scale
result.width = isPositiveNumeric(body.width) ? Number(body.width) : constants.defaultParams.width
result.height = isPositiveNumeric(body.height) ? Number(body.height) : constants.defaultParams.height


result.config = parseJSON(body.config)
if (!hasPropertiesOfObject(result.config, mermaid.mermaidAPI.defaultConfig)) {
return errorOut(400, 'wrong diagram config parameters')
}

return result
}

function parseJSON(blob) {
let parsed = JSON.parse(blob)
while (typeof(parsed) === 'string') {
parsed = JSON.parse(parsed)
}
return parsed
}

module.exports = parse
74 changes: 74 additions & 0 deletions repos/kaleido/js/src/mermaid/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* global Mermaid:false */

const semver = require('semver')
const hasOwn = require('object.hasown')
const constants = require('./constants')
const parse = require('./parse')

if (!Object.hasOwn) {
hasOwn.shim();
}

/**
* @param {object} info : info object
* - data
* - format
* - width
* - height
* - scale
* - config
*/
function render (info) {

let parsed = parse(info);
if (parsed.code !== 0) {
// Bad request return promise with error info
return new Promise((resolve) => {resolve(parsed)})
}

// Set diagram config
mermaid.mermaidAPI.setConfig(parsed.config)

let errorCode = 0
let result = null
let errorMsg = null
let pdfBgColor = null

const done = () => {
if (errorCode !== 0 && !errorMsg) {
errorMsg = constants.statusMsg[errorCode]
}

return {
code: errorCode,
message: errorMsg,
pdfBgColor,
format: parsed.format,
result,
width: parsed.width,
height: parsed.height,
scale: parsed.scale,
}
}

let promise

// TODO create different rendering call for v<10 and v>=10 of mermaidjs ?

promise = mermaid.render("graph", info.data)

let exportPromise = promise.then((imgData) => {
result = imgData.svg
return done()
})

return exportPromise
.catch((err) => {
errorCode = 525
errorMsg = err.message
result = null;
return done()
})
}

module.exports = render
4 changes: 2 additions & 2 deletions repos/kaleido/js/src/plotly/parse.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const cst = require('./constants')
const isPlainObj = require('is-plain-obj')
const isPositiveNumeric = require('./is-positive-numeric')
const isNonEmptyString = require('./is-non-empty-string')
const isPositiveNumeric = require('../utils/is-positive-numeric')
const isNonEmptyString = require('../utils/is-non-empty-string')

const contentFormat = cst.contentFormat
const ACCEPT_HEADER = Object.keys(contentFormat).reduce(function (obj, key) {
Expand Down
Loading