feat(CLI): add a simple cli tool

This commit is contained in:
Estevão Soares dos Santos 2015-07-13 05:09:03 +01:00
parent ba7eb7ebaf
commit f6a33e402c
23 changed files with 301 additions and 67 deletions

2
.gitignore vendored
View File

@ -3,4 +3,4 @@
.DS_Store .DS_Store
node_modules node_modules
npm-debug.log npm-debug.log
localtest.html /*.test.*

View File

@ -1,3 +1,5 @@
Gruntfile.js Gruntfile.js
dist/**/*.js dist/**/*.js
build/**/*.js build/**/*.js
src/options.js
bin/*

View File

@ -20,6 +20,7 @@ module.exports = function (grunt) {
}, },
dist: { dist: {
src: [ src: [
'src/options.js',
'src/showdown.js', 'src/showdown.js',
'src/helpers.js', 'src/helpers.js',
'src/converter.js', 'src/converter.js',

BIN
bin/showdown.js Normal file

Binary file not shown.

BIN
dist/showdown.js vendored

Binary file not shown.

BIN
dist/showdown.js.map vendored

Binary file not shown.

BIN
dist/showdown.min.js vendored

Binary file not shown.

Binary file not shown.

View File

@ -36,7 +36,7 @@
"test": "grunt test" "test": "grunt test"
}, },
"bin": { "bin": {
"showdown": "src/cli.js" "showdown": "bin/showdown.js"
}, },
"devDependencies": { "devDependencies": {
"chai": "^1.10.0", "chai": "^1.10.0",
@ -53,5 +53,8 @@
"quiet-grunt": "^0.2.3", "quiet-grunt": "^0.2.3",
"sinon": "^1.14.1", "sinon": "^1.14.1",
"source-map-support": "^0.2.9" "source-map-support": "^0.2.9"
},
"dependencies": {
"yargs": "^3.15.0"
} }
} }

View File

@ -1,23 +0,0 @@
var Showdown = require('./showdown');
var cli = require('cli');
var fs = require('fs');
var converter = new Showdown.converter();
/*
If an argument is given, treat it as the file to be read.
Otherwise, read from stdin.
*/
if(process.argv.length > 2) {
fs.readFile(process.argv[2], function(err, data) {
if(err) {
console.error("Error: Invalid file");
} else {
console.log(converter.makeHtml(data.toString()));
}
})
} else {
cli.withStdin(function(data) {
this.output(converter.makeHtml(data.toString()));
})
}

30
src/cli/cli.js Normal file
View File

@ -0,0 +1,30 @@
'use strict';
var version = require('../../package.json').version,
yargs = require('yargs');
yargs
.version(version, 'v')
.alias('v', 'version')
.option('h', {
alias: 'help',
description: 'Show help'
})
.usage('Usage: showdown <command> [options]')
.demand(1, 'You must provide a valid command')
.command('makehtml', 'Converts markdown into html')
.example('showdown makehtml -i foo.md -o bar.html', 'Converts \'foo.md\' to \'bar.html\'')
.wrap(yargs.terminalWidth());
var argv = yargs.argv,
command = argv._[0];
if (command === 'makehtml') {
require('./makehtml.cmd.js').run();
} else {
yargs.showHelp();
}
if (argv.help) {
yargs.showHelp();
}
process.exit(0);

6
src/cli/errorexit.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = exports = function errorExit(e) {
'use strict';
console.error('ERROR: ' + e.message);
console.error('Run \'showdown <command> -h\' for help');
process.exit(1);
};

123
src/cli/makehtml.cmd.js Normal file
View File

@ -0,0 +1,123 @@
var yargs = require('yargs'),
fs = require('fs'),
errorExit = require('./errorexit.js'),
showdown = require('../../dist/showdown');
yargs.reset()
.usage('Usage: showdown makehtml [options]')
.example('showdown makehtml -i', 'Reads from stdin and outputs to stdout')
.example('showdown makehtml -i foo.md -o bar.html', 'Reads \'foo.md\' and writes to \'bar.html\'')
//.demand(['i'])
.option('i', {
alias : 'input',
describe: 'Input source. Usually a md file. If omitted or empty, reads from stdin',
type: 'string'
})
.option('o', {
alias : 'output',
describe: 'Output target. Usually a html file. If omitted or empty, writes to stdout',
type: 'string',
default: false
})
.option('u', {
alias : 'encoding',
describe: 'Input encoding',
type: 'string'
})
.option('a', {
alias : 'append',
describe: 'Append data to output instead of overwriting',
type: 'string'
})
.option('e', {
alias : 'extensions',
describe: 'Load the specified extensions. Should be valid paths to node compatible extensions',
type: 'array'
})
.config('c')
.alias('c', 'config')
.help('h')
.alias('h', 'help');
yargs.options(showdown.getDefaultOptions(false));
argv = yargs.argv;
function run() {
'use strict';
var input = '',
enc = 'utf8',
output;
if (argv.encoding) {
enc = argv.encoding;
}
// to avoid passing extensions to converter
delete argv.extensions;
var converter = new showdown.Converter(argv);
// Load extensions
if (argv.e) {
for (var i = 0; i < argv.e.length; ++i) {
loadExtension(argv.e[i], converter);
}
}
if (!argv.i || argv.i === '') {
// 'i' is undefined or empty, read from stdin
try {
var size = fs.fstatSync(process.stdin.fd).size;
input = size > 0 ? fs.readSync(process.stdin.fd, size)[0] : '';
} catch (e) {
var err = new Error('Could not read from stdin, reason: ' + e.message);
errorExit(err);
}
} else {
// 'i' has a value, read from file
try {
input = fs.readFileSync(argv.i, enc);
} catch (err) {
errorExit(err);
}
}
// parse and convert file
output = converter.makeHtml(input);
// Write output
if (!argv.o || argv.o === '') {
// o is undefined or empty, write to stdout
process.stdout.write(output);
// we won't print anything since it would conspurcate stdout and,
// consequently, the outputted file
} else {
// o is has a value, presumably a file, write to it.
// If a flag is passed, it means we should append instead of overwriting.
// Only works with files, obviously
var write = (argv.a) ? fs.appendFileSync : fs.writeFileSync;
try {
write(argv.o, output);
} catch (err) {
errorExit(err);
}
console.error('DONE!');
}
}
function loadExtension(path, converter) {
'use strict';
var ext;
try {
ext = require(path);
converter.addExtension(ext, path);
} catch (e) {
console.error('Could not load extension ' + path + '. Reason:');
console.error(e.message);
}
}
module.exports = exports = {
run: run
};

View File

@ -86,13 +86,16 @@ showdown.Converter = function (converterOptions) {
/** /**
* Parse extension * Parse extension
* @param {*} ext * @param {*} ext
* @param {string} [name='']
* @private * @private
*/ */
function _parseExtension(ext) { function _parseExtension(ext, name) {
name = name || null;
// If it's a string, the extension was previously loaded // If it's a string, the extension was previously loaded
if (showdown.helper.isString(ext)) { if (showdown.helper.isString(ext)) {
ext = showdown.helper.stdExtName(ext); ext = showdown.helper.stdExtName(ext);
name = ext;
// LEGACY_SUPPORT CODE // LEGACY_SUPPORT CODE
if (showdown.extensions[ext]) { if (showdown.extensions[ext]) {
@ -118,8 +121,9 @@ showdown.Converter = function (converterOptions) {
ext = [ext]; ext = [ext];
} }
if (!showdown.validateExtension(ext)) { var validExt = validate(ext, name);
return; if (!validExt.valid) {
throw Error(validExt.error);
} }
for (var i = 0; i < ext.length; ++i) { for (var i = 0; i < ext.length; ++i) {
@ -272,9 +276,11 @@ showdown.Converter = function (converterOptions) {
/** /**
* Add extension to THIS converter * Add extension to THIS converter
* @param {{}} extension * @param {{}} extension
* @param {string} [name=null]
*/ */
this.addExtension = function (extension) { this.addExtension = function (extension, name) {
_parseExtension(extension); name = name || null;
_parseExtension(extension, name);
}; };
/** /**

80
src/options.js Normal file
View File

@ -0,0 +1,80 @@
/**
* Created by Tivie on 13-07-2015.
*/
function getDefaultOpts(simple) {
'use strict';
var defaultOptions = {
omitExtraWLInCodeBlocks: {
default: false,
describe: 'Omit the default extra whiteline added to code blocks',
type: 'boolean'
},
noHeaderId: {
default: false,
describe: 'Turn on/off generated header id',
type: 'boolean'
},
prefixHeaderId: {
default: false,
describe: 'Specify a prefix to generated header ids',
type: 'string'
},
headerLevelStart: {
default: false,
describe: 'The header blocks level start',
type: 'integer'
},
parseImgDimensions: {
default: false,
describe: 'Turn on/off image dimension parsing',
type: 'boolean'
},
simplifiedAutoLink: {
default: false,
describe: 'Turn on/off GFM autolink style',
type: 'boolean'
},
literalMidWordUnderscores: {
default: false,
describe: 'Parse midword underscores as literal underscores',
type: 'boolean'
},
strikethrough: {
default: false,
describe: 'Turn on/off strikethrough support',
type: 'boolean'
},
tables: {
default: false,
describe: 'Turn on/off tables support',
type: 'boolean'
},
tablesHeaderId: {
default: false,
describe: 'Add an id to table headers',
type: 'boolean'
},
ghCodeBlocks: {
default: true,
describe: 'Turn on/off GFM fenced code blocks support',
type: 'boolean'
},
tasklists: {
default: false,
describe: 'Turn on/off GFM tasklist support',
type: 'boolean'
}
};
if (simple === false) {
return JSON.parse(JSON.stringify(defaultOptions));
}
var ret = {};
for (var opt in defaultOptions) {
if (defaultOptions.hasOwnProperty(opt)) {
ret[opt] = defaultOptions[opt].default;
}
}
return ret;
}

View File

@ -6,21 +6,7 @@
var showdown = {}, var showdown = {},
parsers = {}, parsers = {},
extensions = {}, extensions = {},
defaultOptions = { globalOptions = getDefaultOpts(true),
omitExtraWLInCodeBlocks: false,
prefixHeaderId: false,
noHeaderId: false,
headerLevelStart: 1,
parseImgDimensions: false,
simplifiedAutoLink: false,
literalMidWordUnderscores: false,
strikethrough: false,
tables: false,
tablesHeaderId: false,
ghCodeBlocks: true, // true due to historical reasons
tasklists: false
},
globalOptions = JSON.parse(JSON.stringify(defaultOptions)),
flavor = { flavor = {
github: { github: {
omitExtraWLInCodeBlocks: true, omitExtraWLInCodeBlocks: true,
@ -33,7 +19,7 @@ var showdown = {},
ghCodeBlocks: true, ghCodeBlocks: true,
tasklists: true tasklists: true
}, },
vanilla: JSON.parse(JSON.stringify(defaultOptions)) vanilla: getDefaultOpts(true)
}; };
/** /**
@ -88,7 +74,7 @@ showdown.getOptions = function () {
*/ */
showdown.resetOptions = function () { showdown.resetOptions = function () {
'use strict'; 'use strict';
globalOptions = JSON.parse(JSON.stringify(defaultOptions)); globalOptions = getDefaultOpts(true);
}; };
/** /**
@ -110,11 +96,12 @@ showdown.setFlavor = function (name) {
/** /**
* Get the default options * Get the default options
* @static * @static
* @param {boolean} [simple=true]
* @returns {{}} * @returns {{}}
*/ */
showdown.getDefaultOptions = function () { showdown.getDefaultOptions = function (simple) {
'use strict'; 'use strict';
return defaultOptions; return getDefaultOpts(simple);
}; };
/** /**
@ -233,7 +220,7 @@ function validate(extension, name) {
} }
for (var i = 0; i < extension.length; ++i) { for (var i = 0; i < extension.length; ++i) {
var baseMsg = errMsg + 'sub-extension ' + i + ': ', var baseMsg = errMsg + ' sub-extension ' + i + ': ',
ext = extension[i]; ext = extension[i];
if (typeof ext !== 'object') { if (typeof ext !== 'object') {
ret.valid = false; ret.valid = false;

View File

@ -1,7 +1,8 @@
showdown.subParser('headers', function (text, options, globals) { showdown.subParser('headers', function (text, options, globals) {
'use strict'; 'use strict';
var prefixHeader = options.prefixHeaderId; var prefixHeader = options.prefixHeaderId,
headerLevelStart = (isNaN(parseInt(options.headerLevelStart))) ? 1 : parseInt(options.headerLevelStart);
// Set text-style headers: // Set text-style headers:
// Header 1 // Header 1
@ -14,7 +15,7 @@ showdown.subParser('headers', function (text, options, globals) {
var spanGamut = showdown.subParser('spanGamut')(m1, options, globals), var spanGamut = showdown.subParser('spanGamut')(m1, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"', hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"',
hLevel = parseInt(options.headerLevelStart), hLevel = headerLevelStart,
hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>'; hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>';
return showdown.subParser('hashBlock')(hashBlock, options, globals); return showdown.subParser('hashBlock')(hashBlock, options, globals);
}); });
@ -22,7 +23,7 @@ showdown.subParser('headers', function (text, options, globals) {
text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, function (matchFound, m1) { text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, function (matchFound, m1) {
var spanGamut = showdown.subParser('spanGamut')(m1, options, globals), var spanGamut = showdown.subParser('spanGamut')(m1, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"', hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"',
hLevel = parseInt(options.headerLevelStart) + 1, hLevel = headerLevelStart + 1,
hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>'; hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>';
return showdown.subParser('hashBlock')(hashBlock, options, globals); return showdown.subParser('hashBlock')(hashBlock, options, globals);
}); });
@ -49,7 +50,7 @@ showdown.subParser('headers', function (text, options, globals) {
text = text.replace(/^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm, function (wholeMatch, m1, m2) { text = text.replace(/^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm, function (wholeMatch, m1, m2) {
var span = showdown.subParser('spanGamut')(m2, options, globals), var span = showdown.subParser('spanGamut')(m2, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m2) + '"', hID = (options.noHeaderId) ? '' : ' id="' + headerId(m2) + '"',
hLevel = parseInt(options.headerLevelStart) - 1 + m1.length, hLevel = headerLevelStart - 1 + m1.length,
header = '<h' + hLevel + hID + '>' + span + '</h' + hLevel + '>'; header = '<h' + hLevel + hID + '>' + span + '</h' + hLevel + '>';
return showdown.subParser('hashBlock')(header, options, globals); return showdown.subParser('hashBlock')(header, options, globals);

3
test/cli/basic.html Normal file
View File

@ -0,0 +1,3 @@
<h1 id="sometitle">some title</h1>
<p>Test <strong>bold</strong> and <em>italic</em></p>

3
test/cli/basic.md Normal file
View File

@ -0,0 +1,3 @@
# some title
Test **bold** and _italic_

13
test/node/cli.js Normal file
View File

@ -0,0 +1,13 @@
var execSync = require('child_process').execSync;
describe('showdown cli', function () {
'use strict';
it('basic stdin stdout', function () {
var otp = execSync('showdown makehtml', {
encoding: 'utf8',
input: '**foo**'
});
otp.trim().should.equal('<p><strong>foo</strong></p>');
});
});

View File

@ -17,6 +17,8 @@ describe('showdown.options', function () {
describe('getDefaultOptions()', function () { describe('getDefaultOptions()', function () {
it('should get default options', function () { it('should get default options', function () {
var opts = require('../optionswp').getDefaultOpts(true);
/*
var opts = { var opts = {
omitExtraWLInCodeBlocks: false, omitExtraWLInCodeBlocks: false,
prefixHeaderId: false, prefixHeaderId: false,
@ -31,6 +33,7 @@ describe('showdown.options', function () {
ghCodeBlocks: true, ghCodeBlocks: true,
tasklists: false tasklists: false
}; };
*/
expect(showdown.getDefaultOptions()).to.be.eql(opts); expect(showdown.getDefaultOptions()).to.be.eql(opts);
}); });
}); });

9
test/optionswp.js Normal file
View File

@ -0,0 +1,9 @@
/* jshint ignore:start */
var fs = require('fs'),
filedata;
filedata = fs.readFileSync('src/options.js', 'utf8');
eval(filedata);
module.exports = {
getDefaultOpts: getDefaultOpts
};
/* jshint ignore:end */

View File

@ -1,13 +0,0 @@
/**
* Created by Estevao on 10-07-2015.
*/
/*
var showdown = require('../dist/showdown.js'),
bootstrap = require('./bootstrap.js'),
assertion = bootstrap.assertion,
testsuite = bootstrap.getTestSuite('test/features/');
describe('makeHtml() single test', function () {
'use strict';
});
*/