Adds CLI for rendering mermaid files

This commit is contained in:
fardog 2014-12-20 17:18:38 -08:00
parent 9458bfb24f
commit c703c9a0d3
5 changed files with 424 additions and 1 deletions

27
bin/mermaid.js Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env node
var fs = require('fs')
, chalk = require('chalk')
, error = chalk.bold.red
, cli = require('../lib/cli.js')
, lib = require('../lib')
cli.parse(process.argv.slice(2), function(err, message, options) {
if (err) {
console.error(
error('\nYou had errors in your syntax. Use --help for further information.')
)
err.forEach(function (e) {
console.error(e.message)
})
return
}
else if (message) {
console.log(message)
return
}
lib.process(options.files, options)
})

135
lib/cli.js Normal file
View File

@ -0,0 +1,135 @@
var fs = require('fs')
, exec = require('child_process').exec
, chalk = require('chalk')
, which = require('which')
, parseArgs = require('minimist')
, semver = require('semver')
var PHANTOM_VERSION = "^1.9.0"
var info = chalk.blue.bold
, note = chalk.green.bold
var cli = function(options) {
this.options = {
alias: {
help: 'h'
, png: 'p'
, outputDir: 'o'
, svg: 's'
, verbose: 'v'
, phantomPath: 'e'
}
, 'boolean': ['help', 'png', 'svg']
, 'string': ['outputDir']
}
this.errors = []
this.message = null
this.helpMessage = [
, info('Usage: mermaid [options] <file>...')
, ""
, "file The mermaid description file to be rendered"
, ""
, "Options:"
, " -s --svg Output SVG instead of PNG (experimental)"
, " -p --png If SVG was selected, and you also want PNG, set this flag"
, " -o --outputDir Directory to save files, will be created automatically, defaults to `cwd`"
, " -e --phantomPath Specify the path to the phantomjs executable"
, " -h --help Show this message"
, " -v --verbose Show logging"
, " --version Print version and quit"
]
return this
}
cli.prototype.parse = function(argv, next) {
var options = parseArgs(argv, this.options)
, phantom
if (options.version) {
var pkg = require('../package.json')
this.message = "" + pkg.version
}
else if (options.help) {
this.message = this.helpMessage.join('\n')
}
else {
options.files = options._
if (!options.files.length) {
this.errors.push(new Error("You must specify at least one source file."))
}
// ensure that parameter-expecting options have parameters
;['outputDir', 'phantomPath'].forEach(function(i) {
if(typeof options[i] !== 'undefined') {
if (typeof options[i] !== 'string' || options[i].length < 1) {
this.errors.push(new Error(i + " expects a value."))
}
}
}.bind(this))
}
if (options.svg && !options.png) {
options.png = false
}
else {
options.png = true
}
// If phantom hasn't been specified, see if we can find it
if (!options.phantomPath) {
try {
var phantom = require('phantomjs')
options.phantomPath = phantom.path
} catch (e) {
try {
options.phantomPath = which.sync('phantomjs')
} catch (e) {
if (!options.phantomPath) {
var err = [
"Cannot find phantomjs in your PATH. If phantomjs is installed"
, "you may need to specify its path manually with the '-e' option."
, "If it is not installed, you should view the README for further"
, "details."
]
this.errors.push(new Error(err.join('\n')))
next(
this.errors.length > 0 ? this.errors : null
, this.message
, options)
return
}
}
}
}
// If we have phantompath, see if its version satisfies our requirements
exec(options.phantomPath + ' --version', function(err, stdout, stderr) {
if (err) {
this.errors.push(
new Error("Could not find phantomjs at the specified path.")
)
}
else if (!semver.satisfies(stdout, PHANTOM_VERSION)) {
this.message = note(
'mermaid requires phantomjs '
+ PHANTOM_VERSION
+ ' to be installed, found version '
+ stdout
)
}
next(this.errors.length > 0 ? this.errors : null, this.message, options)
}.bind(this))
}
module.exports = function() {
return new cli()
}()

40
lib/index.js Normal file
View File

@ -0,0 +1,40 @@
var os = require('os')
, fs = require('fs')
, path = require('path')
, spawn = require('child_process').spawn
var mkdirp = require('mkdirp')
var phantomscript = path.join(__dirname, 'phantomscript.js')
module.exports = { process: processMermaid }
function processMermaid(files, _options, _next) {
var options = _options || {}
, outputDir = options.outputDir || process.cwd()
, next = _next || function() {}
, phantomArgs = [
phantomscript
, outputDir
, options.png
, options.svg
, options.verbose
]
files.forEach(function(file) {
phantomArgs.push(file)
})
mkdirp(outputDir, function(err) {
if (err) {
throw err
return
}
phantom = spawn(options.phantomPath, phantomArgs)
phantom.on('exit', next)
phantom.stderr.pipe(process.stderr)
phantom.stdout.pipe(process.stdout)
})
}

213
lib/phantomscript.js Normal file
View File

@ -0,0 +1,213 @@
/**
* Credits:
* - SVG Processing from the NYTimes svg-crowbar, under an MIT license
* https://github.com/NYTimes/svg-crowbar
* - Thanks to the grunticon project for some guidance
* https://github.com/filamentgroup/grunticon
*/
phantom.onError = function(msg, trace) {
var msgStack = ['PHANTOM ERROR: ' + msg]
if (trace && trace.length) {
msgStack.push('TRACE:')
trace.forEach(function(t) {
msgStack.push(
' -> '
+ (t.file || t.sourceURL)
+ ': '
+ t.line
+ (t.function ? ' (in function ' + t.function +')' : '')
)
})
}
system.stderr.write(msgStack.join('\n'))
phantom.exit(1)
}
var system = require('system')
, fs = require('fs')
, webpage = require('webpage')
var page = webpage.create()
, files = phantom.args.slice(4, phantom.args.length)
, options = {
outputDir: phantom.args[0]
, png: phantom.args[1] === 'true' ? true : false
, svg: phantom.args[2] === 'true' ? true : false
, verbose: phantom.args[3] === 'true' ? true : false
}
, log = logger(options.verbose)
page.content = [
'<html>'
, '<head>'
, '<style type="text/css">'
, '* { margin: 0; padding: 0; }'
, '</style>'
, '</head>'
, '<body>'
, '</body>'
, '</html>'
].join('\n')
page.injectJs('../dist/mermaid.full.js')
files.forEach(function(file) {
var contents = fs.read(file)
, filename = file.split(fs.separator).slice(-1)
, oParser = new DOMParser()
, oDOM
, svgContent
, allElements
// this JS is executed in this statement is sandboxed, even though it doesn't
// look like it. we need to serialize then unserialize the svgContent that's
// taken from the DOM
svgContent = page.evaluate(executeInPage, contents)
oDOM = oParser.parseFromString(svgContent, "text/xml")
resolveSVGElement(oDOM.firstChild)
// traverse the SVG, and replace all foreignObject elements
// can be removed when https://github.com/knsv/mermaid/issues/58 is resolved
allElements = traverse(oDOM)
for (var i = 0, len = allElements.length; i < len; i++) {
resolveForeignObjects(allElements[i])
}
if (options.png) {
page.viewportSize = {
width: ~~oDOM.documentElement.attributes.getNamedItem('width').value
, height: ~~oDOM.documentElement.attributes.getNamedItem('height').value
}
page.render(options.outputDir + fs.separator + filename + '.png')
log('saved png: ' + filename + '.png')
}
if (options.svg) {
var serialize = new XMLSerializer()
fs.write(
options.outputDir + fs.separator + filename + '.svg'
, serialize.serializeToString(oDOM)
, 'w'
)
log('saved svg: ' + filename + '.svg')
}
})
phantom.exit()
function logger(_verbose) {
var verbose = _verbose
return function(_message, _level) {
var level = level
, message = _message
, log
log = level === 'error' ? system.stderr : system.stdout
if (verbose) {
log.write(message + '\n')
}
}
}
function traverse(obj){
var tree = []
tree.push(obj)
visit(obj)
function visit(node) {
if (node && node.hasChildNodes()) {
var child = node.firstChild
while (child) {
if (child.nodeType === 1 && child.nodeName != 'SCRIPT'){
tree.push(child)
visit(child)
}
child = child.nextSibling
}
}
}
return tree
}
function resolveSVGElement(element) {
var prefix = {
xmlns: "http://www.w3.org/2000/xmlns/"
, xlink: "http://www.w3.org/1999/xlink"
, svg: "http://www.w3.org/2000/svg"
}
, doctype = '<!DOCTYPE svg:svg PUBLIC'
+ ' "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN"'
+ ' "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">'
element.setAttribute("version", "1.1")
// removing attributes so they aren't doubled up
element.removeAttribute("xmlns")
element.removeAttribute("xlink")
// These are needed for the svg
if (!element.hasAttributeNS(prefix.xmlns, "xmlns")) {
element.setAttributeNS(prefix.xmlns, "xmlns", prefix.svg)
}
if (!element.hasAttributeNS(prefix.xmlns, "xmlns:xlink")) {
element.setAttributeNS(prefix.xmlns, "xmlns:xlink", prefix.xlink)
}
}
function resolveForeignObjects(element) {
var children
, textElement
, textSpan
if (element.tagName === 'foreignObject') {
textElement = document.createElement('text')
textSpan = document.createElement('tspan')
textSpan.setAttribute(
'style'
, 'font-size: 11.5pt; font-family: "sans-serif";'
)
textSpan.setAttribute('x', 0)
textSpan.setAttribute('y', 14.5)
textSpan.textContent = element.textContent
textElement.appendChild(textSpan)
element.parentElement.appendChild(textElement)
element.parentElement.removeChild(element)
}
}
// The sandboxed function that's executed in-page by phantom
function executeInPage(contents) {
var xmlSerializer = new XMLSerializer()
, toRemove
, el
, elContent
, svg
, svgValue
toRemove = document.getElementsByClassName('mermaid')
if (toRemove && toRemove.length) {
for (var i = 0, len = toRemove.length; i < len; i++) {
toRemove[i].parentNode.removeChild(toRemove[i])
}
}
el = document.createElement("div")
el.className = 'mermaid'
elContent = document.createTextNode(contents)
el.appendChild(elContent)
document.body.appendChild(el)
mermaid.init()
svg = document.querySelector('svg')
svgValue = xmlSerializer.serializeToString(svg)
return svgValue
}

View File

@ -3,6 +3,9 @@
"version": "0.2.16",
"description": "Markdownish syntax for generating flowcharts",
"main": "src/main.js",
"bin": {
"mermaid": "./bin/mermaid.js"
},
"scripts": {
"test": "gulp coverage"
},
@ -13,8 +16,13 @@
"author": "",
"license": "MIT",
"dependencies": {
"chalk": "^0.5.1",
"dagre-d3": "~0.3.2",
"he": "^0.5.0",
"dagre-d3": "~0.3.2"
"minimist": "^1.1.0",
"mkdirp": "^0.5.0",
"semver": "^4.1.1",
"which": "^1.0.8"
},
"devDependencies": {
"browserify": "~6.2.0",