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 412 additions and 101 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

2
bin/showdown.js Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../src/cli/cli');

135
dist/showdown.js vendored
View File

@ -1,5 +1,86 @@
;/*! showdown 12-07-2015 */
;/*! showdown 13-07-2015 */
(function(){
/**
* 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;
}
/**
* Created by Tivie on 06-01-2015.
*/
@ -8,21 +89,7 @@
var showdown = {},
parsers = {},
extensions = {},
defaultOptions = {
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)),
globalOptions = getDefaultOpts(true),
flavor = {
github: {
omitExtraWLInCodeBlocks: true,
@ -35,7 +102,7 @@ var showdown = {},
ghCodeBlocks: true,
tasklists: true
},
vanilla: JSON.parse(JSON.stringify(defaultOptions))
vanilla: getDefaultOpts(true)
};
/**
@ -90,7 +157,7 @@ showdown.getOptions = function () {
*/
showdown.resetOptions = function () {
'use strict';
globalOptions = JSON.parse(JSON.stringify(defaultOptions));
globalOptions = getDefaultOpts(true);
};
/**
@ -112,11 +179,12 @@ showdown.setFlavor = function (name) {
/**
* Get the default options
* @static
* @param {boolean} [simple=true]
* @returns {{}}
*/
showdown.getDefaultOptions = function () {
showdown.getDefaultOptions = function (simple) {
'use strict';
return defaultOptions;
return getDefaultOpts(simple);
};
/**
@ -235,7 +303,7 @@ function validate(extension, name) {
}
for (var i = 0; i < extension.length; ++i) {
var baseMsg = errMsg + 'sub-extension ' + i + ': ',
var baseMsg = errMsg + ' sub-extension ' + i + ': ',
ext = extension[i];
if (typeof ext !== 'object') {
ret.valid = false;
@ -532,13 +600,16 @@ showdown.Converter = function (converterOptions) {
/**
* Parse extension
* @param {*} ext
* @param {string} [name='']
* @private
*/
function _parseExtension(ext) {
function _parseExtension(ext, name) {
name = name || null;
// If it's a string, the extension was previously loaded
if (showdown.helper.isString(ext)) {
ext = showdown.helper.stdExtName(ext);
name = ext;
// LEGACY_SUPPORT CODE
if (showdown.extensions[ext]) {
@ -564,8 +635,9 @@ showdown.Converter = function (converterOptions) {
ext = [ext];
}
if (!showdown.validateExtension(ext)) {
return;
var validExt = validate(ext, name);
if (!validExt.valid) {
throw Error(validExt.error);
}
for (var i = 0; i < ext.length; ++i) {
@ -718,9 +790,11 @@ showdown.Converter = function (converterOptions) {
/**
* Add extension to THIS converter
* @param {{}} extension
* @param {string} [name=null]
*/
this.addExtension = function (extension) {
_parseExtension(extension);
this.addExtension = function (extension, name) {
name = name || null;
_parseExtension(extension, name);
};
/**
@ -1497,7 +1571,8 @@ showdown.subParser('hashHTMLBlocks', function (text, options, globals) {
showdown.subParser('headers', function (text, options, globals) {
'use strict';
var prefixHeader = options.prefixHeaderId;
var prefixHeader = options.prefixHeaderId,
headerLevelStart = (isNaN(parseInt(options.headerLevelStart))) ? 1 : parseInt(options.headerLevelStart);
// Set text-style headers:
// Header 1
@ -1510,7 +1585,7 @@ showdown.subParser('headers', function (text, options, globals) {
var spanGamut = showdown.subParser('spanGamut')(m1, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"',
hLevel = parseInt(options.headerLevelStart),
hLevel = headerLevelStart,
hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>';
return showdown.subParser('hashBlock')(hashBlock, options, globals);
});
@ -1518,7 +1593,7 @@ showdown.subParser('headers', function (text, options, globals) {
text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, function (matchFound, m1) {
var spanGamut = showdown.subParser('spanGamut')(m1, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"',
hLevel = parseInt(options.headerLevelStart) + 1,
hLevel = headerLevelStart + 1,
hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>';
return showdown.subParser('hashBlock')(hashBlock, options, globals);
});
@ -1545,7 +1620,7 @@ showdown.subParser('headers', function (text, options, globals) {
text = text.replace(/^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm, function (wholeMatch, m1, m2) {
var span = showdown.subParser('spanGamut')(m2, options, globals),
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 + '>';
return showdown.subParser('hashBlock')(header, options, globals);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@
"test": "grunt test"
},
"bin": {
"showdown": "src/cli.js"
"showdown": "bin/showdown.js"
},
"devDependencies": {
"chai": "^1.10.0",
@ -53,5 +53,8 @@
"quiet-grunt": "^0.2.3",
"sinon": "^1.14.1",
"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
* @param {*} ext
* @param {string} [name='']
* @private
*/
function _parseExtension(ext) {
function _parseExtension(ext, name) {
name = name || null;
// If it's a string, the extension was previously loaded
if (showdown.helper.isString(ext)) {
ext = showdown.helper.stdExtName(ext);
name = ext;
// LEGACY_SUPPORT CODE
if (showdown.extensions[ext]) {
@ -118,8 +121,9 @@ showdown.Converter = function (converterOptions) {
ext = [ext];
}
if (!showdown.validateExtension(ext)) {
return;
var validExt = validate(ext, name);
if (!validExt.valid) {
throw Error(validExt.error);
}
for (var i = 0; i < ext.length; ++i) {
@ -272,9 +276,11 @@ showdown.Converter = function (converterOptions) {
/**
* Add extension to THIS converter
* @param {{}} extension
* @param {string} [name=null]
*/
this.addExtension = function (extension) {
_parseExtension(extension);
this.addExtension = function (extension, name) {
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 = {},
parsers = {},
extensions = {},
defaultOptions = {
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)),
globalOptions = getDefaultOpts(true),
flavor = {
github: {
omitExtraWLInCodeBlocks: true,
@ -33,7 +19,7 @@ var showdown = {},
ghCodeBlocks: true,
tasklists: true
},
vanilla: JSON.parse(JSON.stringify(defaultOptions))
vanilla: getDefaultOpts(true)
};
/**
@ -88,7 +74,7 @@ showdown.getOptions = function () {
*/
showdown.resetOptions = function () {
'use strict';
globalOptions = JSON.parse(JSON.stringify(defaultOptions));
globalOptions = getDefaultOpts(true);
};
/**
@ -110,11 +96,12 @@ showdown.setFlavor = function (name) {
/**
* Get the default options
* @static
* @param {boolean} [simple=true]
* @returns {{}}
*/
showdown.getDefaultOptions = function () {
showdown.getDefaultOptions = function (simple) {
'use strict';
return defaultOptions;
return getDefaultOpts(simple);
};
/**
@ -233,7 +220,7 @@ function validate(extension, name) {
}
for (var i = 0; i < extension.length; ++i) {
var baseMsg = errMsg + 'sub-extension ' + i + ': ',
var baseMsg = errMsg + ' sub-extension ' + i + ': ',
ext = extension[i];
if (typeof ext !== 'object') {
ret.valid = false;

View File

@ -1,7 +1,8 @@
showdown.subParser('headers', function (text, options, globals) {
'use strict';
var prefixHeader = options.prefixHeaderId;
var prefixHeader = options.prefixHeaderId,
headerLevelStart = (isNaN(parseInt(options.headerLevelStart))) ? 1 : parseInt(options.headerLevelStart);
// Set text-style headers:
// Header 1
@ -14,7 +15,7 @@ showdown.subParser('headers', function (text, options, globals) {
var spanGamut = showdown.subParser('spanGamut')(m1, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"',
hLevel = parseInt(options.headerLevelStart),
hLevel = headerLevelStart,
hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>';
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) {
var spanGamut = showdown.subParser('spanGamut')(m1, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"',
hLevel = parseInt(options.headerLevelStart) + 1,
hLevel = headerLevelStart + 1,
hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>';
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) {
var span = showdown.subParser('spanGamut')(m2, options, globals),
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 + '>';
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 () {
it('should get default options', function () {
var opts = require('../optionswp').getDefaultOpts(true);
/*
var opts = {
omitExtraWLInCodeBlocks: false,
prefixHeaderId: false,
@ -31,6 +33,7 @@ describe('showdown.options', function () {
ghCodeBlocks: true,
tasklists: false
};
*/
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';
});
*/