mirror of
https://github.com/astefanutti/decktape.git
synced 2024-11-22 12:22:10 +03:00
Use Docopt for CLI definition and parsing
This commit is contained in:
parent
27890ea897
commit
e45d4bdd91
289
decktape.js
289
decktape.js
@ -1,10 +1,6 @@
|
||||
require.paths.push(phantom.libraryPath + '/libs/');
|
||||
|
||||
var page = require('webpage').create(),
|
||||
printer = require('printer').create(),
|
||||
system = require('system'),
|
||||
fs = require('fs'),
|
||||
Promise = require('promise');
|
||||
var system = require('system');
|
||||
|
||||
// Node to PhantomJS bridging
|
||||
var process = {
|
||||
@ -15,87 +11,202 @@ var process = {
|
||||
//stdout: system.stdout,
|
||||
exit: phantom.exit
|
||||
};
|
||||
// As opposed to PhantomJS, global variables declared in the main script are not accessible
|
||||
// in modules loaded with require
|
||||
|
||||
// As opposed to PhantomJS, global variables declared in the main script are not
|
||||
// accessible in modules loaded with require
|
||||
if (system.platform === 'slimerjs')
|
||||
require.globals.process = process;
|
||||
|
||||
var docopt = require('docopt'),
|
||||
chalk = require('chalk'),
|
||||
fs = require('fs'),
|
||||
page = require('webpage').create(),
|
||||
printer = require('printer').create(),
|
||||
Promise = require('promise');
|
||||
|
||||
var plugins = loadAvailablePlugins(phantom.libraryPath + '/plugins/');
|
||||
|
||||
var parser = require('nomnom')
|
||||
.script('phantomjs decktape.js')
|
||||
.options({
|
||||
url: {
|
||||
position: 1,
|
||||
required: true,
|
||||
help: 'URL of the slides deck'
|
||||
},
|
||||
filename: {
|
||||
position: 2,
|
||||
required: true,
|
||||
help: 'Filename of the output PDF file'
|
||||
},
|
||||
size: {
|
||||
abbr: 's',
|
||||
callback: parseResolution,
|
||||
transform: parseResolution,
|
||||
help: 'Size of the slides deck viewport: <width>x<height>'
|
||||
},
|
||||
pause: {
|
||||
abbr: 'p',
|
||||
default: 1000,
|
||||
help: 'Duration in milliseconds before each slide is exported'
|
||||
},
|
||||
loadpause: {
|
||||
default: 0,
|
||||
help: 'Duration in milliseconds between the page has loaded and starting to export slides'
|
||||
},
|
||||
screenshots: {
|
||||
default: false,
|
||||
flag: true,
|
||||
help: 'Capture each slide as an image'
|
||||
},
|
||||
screenshotDirectory: {
|
||||
full: 'screenshots-directory',
|
||||
default: 'screenshots',
|
||||
help: 'Screenshots output directory'
|
||||
},
|
||||
screenshotSize: {
|
||||
full: 'screenshots-size',
|
||||
list: true,
|
||||
callback: parseResolution,
|
||||
transform: parseResolution,
|
||||
help: 'Screenshots resolution, can be repeated'
|
||||
},
|
||||
screenshotFormat: {
|
||||
full: 'screenshots-format',
|
||||
default: 'png',
|
||||
choices: ['jpg', 'png'],
|
||||
help: 'Screenshots image format, one of [jpg, png]'
|
||||
}
|
||||
});
|
||||
parser.nocommand()
|
||||
.help('Defaults to the automatic command.\n' +
|
||||
'Iterates over the available plugins, picks the compatible one for presentation at the \n' +
|
||||
'specified <url> and uses it to export and write the PDF into the specified <filename>.');
|
||||
parser.command('automatic')
|
||||
.help('Iterates over the available plugins, picks the compatible one for presentation at the \n' +
|
||||
'specified <url> and uses it to export and write the PDF into the specified <filename>.');
|
||||
Object.keys(plugins).forEach(function (id) {
|
||||
var command = parser.command(id);
|
||||
if (typeof plugins[id].options === 'object')
|
||||
command.options(plugins[id].options);
|
||||
if (typeof plugins[id].help === 'string')
|
||||
command.help(plugins[id].help);
|
||||
});
|
||||
// TODO: should be deactivated as well when PhantomJS does not execute in a TTY context
|
||||
if (system.os.name === 'windows')
|
||||
parser.nocolors();
|
||||
var cmd = [
|
||||
' ',
|
||||
'Usage: ',
|
||||
' decktape.js [options] [plugin] URL FILE ',
|
||||
' decktape.js [plugin] -h ',
|
||||
' ',
|
||||
'Command: ',
|
||||
' plugin One of: automatic, bespoke, [default: automatic]',
|
||||
' csss, deck, dzslides, ',
|
||||
' flowtime, generic, impress, ',
|
||||
' remark, reveal, shower, ',
|
||||
' slidy ',
|
||||
' ',
|
||||
" See 'decktape.js plugin -h' to read about a specific plugin options ",
|
||||
' ',
|
||||
' The default automatic plugin iterates over the available plugins, picks the ',
|
||||
' compatible one for the presentation at the specified URL. ',
|
||||
' ',
|
||||
'Arguments: ',
|
||||
' URL URL of the slides deck ',
|
||||
' FILE Filename of the output PDF ',
|
||||
' file ',
|
||||
' ',
|
||||
'Options: ',
|
||||
' -s, --size=SIZE Size of the slides deck [default: 1280x720]',
|
||||
' viewport may vary per plugin',
|
||||
' -p, --pause=MS Duration in milliseconds [default: 1000]',
|
||||
' before each slide is ',
|
||||
' exported ',
|
||||
' --load-pause=MS Duration in milliseconds [default: 0]',
|
||||
' between the page has loaded ',
|
||||
' and starting exporting ',
|
||||
' slides ',
|
||||
' -h, --help show this help message ',
|
||||
' and exit '
|
||||
];
|
||||
|
||||
var options = parser.parse(system.args.slice(1));
|
||||
var spec = [
|
||||
{ regex: /\[plugin]/g, replace: Object.keys(plugins)
|
||||
.reduce(function (l, p) {
|
||||
return l + ' | ' + p;
|
||||
}, '([automatic]') + ')'
|
||||
},
|
||||
{ regex: /\[default: 1280x720]/, replace: '' }
|
||||
];
|
||||
|
||||
var help = [
|
||||
{ regex: /decktape\.js \[options] .+ URL FILE/, style: chalk.inverse.bold.white },
|
||||
{ regex: /^(.*)(See|The default|compatible)(.*)$/mg, style: chalk.gray },
|
||||
{ regex: /(plugin)( -h)/, style: [chalk.underline, null] },
|
||||
{ regex: /^(\S+:)/gm, style: chalk.bold.cyan },
|
||||
{ regex: /\[default: (.+)]/g, style: chalk.gray },
|
||||
{ regex: /may vary per plugin/, style: chalk.gray.dim },
|
||||
{ regex: new RegExp('(automatic(?=,)|' + Object.keys(plugins).join('(?=,)|') + ')', 'g'),
|
||||
style: chalk.underline }
|
||||
];
|
||||
|
||||
function format(cmd, rules) {
|
||||
return rules.reduce(function (cmd, rule) {
|
||||
if (typeof rule.replace === 'function')
|
||||
return cmd.replace(rule.regex, rule.replace);
|
||||
if (typeof rule.replace === 'string')
|
||||
return cmd.replace(rule.regex, rule.replace);
|
||||
// TODO: should be deactivated as well when PhantomJS does not execute in a TTY context
|
||||
if (system.os.name !== 'windows') {
|
||||
if (typeof rule.style === 'function') {
|
||||
return cmd.replace(rule.regex, function (match) {
|
||||
return rule.style.call({}, match);
|
||||
});
|
||||
} else if (Array.isArray(rule.style)) {
|
||||
return cmd.replace(rule.regex, function () {
|
||||
var match = arguments;
|
||||
return rule.style.reduce(function (c, s, i) {
|
||||
return c + (s !== null ? s.call({}, match[i + 1]) : match[i + 1]);
|
||||
}, '');
|
||||
});
|
||||
}
|
||||
}
|
||||
}, cmd.reduce(function (cmd, row) {
|
||||
if (Array.isArray(row))
|
||||
if (row.length > 0)
|
||||
return cmd + '\n' + row.join('\n') + '\n';
|
||||
else
|
||||
return cmd;
|
||||
else
|
||||
return cmd + row + '\n';
|
||||
}, ''));
|
||||
}
|
||||
|
||||
var options;
|
||||
try {
|
||||
options = docopt.docopt(format(cmd, spec), {
|
||||
argv: system.args.slice(1),
|
||||
options_first: false,
|
||||
help: false,
|
||||
exit: false
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(format(cmd.slice(1, 3), [{ regex: /^(.*)/mg, style: chalk.red }]));
|
||||
console.log('See \'decktape.js -h\' for more details');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (var id in options) {
|
||||
if (typeof plugins[id] === 'object' && options[id])
|
||||
options.plugin = id;
|
||||
if (id === '--size' && options[id])
|
||||
options.size = parseResolution(options[id]);
|
||||
}
|
||||
|
||||
if (options.plugin) {
|
||||
var plugin = plugins[options.plugin];
|
||||
cmd = [
|
||||
docopt.parse_section('Usage: ', format(cmd, [{ regex: /\[plugin]/g, replace: options.plugin || '' }])),
|
||||
docopt.parse_section('Command:', format(plugin.cmd || [], [])),
|
||||
docopt.parse_section('Options:', format(cmd, [])).concat(
|
||||
docopt.parse_section('Options:', format(plugin.cmd || [], [])).map(function (l) { return l.replace(/^Options:.*\n/, '') }))
|
||||
];
|
||||
}
|
||||
|
||||
if (options['--help']) {
|
||||
if (options.plugin)
|
||||
console.log(format(cmd, help.concat(plugins[options.plugin].help || [])));
|
||||
else
|
||||
console.log(format(cmd, help));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(chalk.dim(JSON.stringify(options)));
|
||||
|
||||
//var parser = require('nomnom')
|
||||
// .script('phantomjs decktape.js')
|
||||
// .options({
|
||||
// url: {
|
||||
// position: 1,
|
||||
// required: true,
|
||||
// help: 'URL of the slides deck'
|
||||
// },
|
||||
// filename: {
|
||||
// position: 2,
|
||||
// required: true,
|
||||
// help: 'Filename of the output PDF file'
|
||||
// },
|
||||
// size: {
|
||||
// abbr: 's',
|
||||
// callback: parseResolution,
|
||||
// transform: parseResolution,
|
||||
// help: 'Size of the slides deck viewport: <width>x<height>'
|
||||
// },
|
||||
// pause: {
|
||||
// abbr: 'p',
|
||||
// default: 1000,
|
||||
// help: 'Duration in milliseconds before each slide is exported'
|
||||
// },
|
||||
// screenshots: {
|
||||
// default: false,
|
||||
// flag: true,
|
||||
// help: 'Capture each slide as an image'
|
||||
// },
|
||||
// screenshotDirectory: {
|
||||
// full: 'screenshots-directory',
|
||||
// default: 'screenshots',
|
||||
// help: 'Screenshots output directory'
|
||||
// },
|
||||
// screenshotSize: {
|
||||
// full: 'screenshots-size',
|
||||
// list: true,
|
||||
// callback: parseResolution,
|
||||
// transform: parseResolution,
|
||||
// help: 'Screenshots resolution, can be repeated'
|
||||
// },
|
||||
// screenshotFormat: {
|
||||
// full: 'screenshots-format',
|
||||
// default: 'png',
|
||||
// choices: ['jpg', 'png'],
|
||||
// help: 'Screenshots image format, one of [jpg, png]'
|
||||
// }
|
||||
// });
|
||||
|
||||
page.onLoadStarted = function () {
|
||||
console.log('Loading page ' + options.url + ' ...');
|
||||
console.log('Loading page ' + options['URL'] + ' ...');
|
||||
};
|
||||
|
||||
page.onResourceTimeout = function (request) {
|
||||
@ -118,15 +229,15 @@ page.onConsoleMessage = function (msg) {
|
||||
console.log(msg);
|
||||
};
|
||||
|
||||
page.open(options.url, function (status) {
|
||||
page.open(options['URL'], function (status) {
|
||||
if (status !== 'success') {
|
||||
console.log('Unable to load the address: ' + options.url);
|
||||
console.log('Unable to load the address: ' + options['URL']);
|
||||
phantom.exit(1);
|
||||
}
|
||||
|
||||
if (options.loadpause > 0)
|
||||
if (options['--load-pause'] > 0)
|
||||
Promise.resolve()
|
||||
.then(delay(options.loadpause))
|
||||
.then(delay(options['--load-pause']))
|
||||
.then(exportSlides);
|
||||
else
|
||||
exportSlides();
|
||||
@ -134,16 +245,16 @@ page.open(options.url, function (status) {
|
||||
|
||||
function exportSlides() {
|
||||
var plugin;
|
||||
if (!options.command || options.command === 'automatic') {
|
||||
if (!options.plugin || options.plugin === 'automatic') {
|
||||
plugin = createActivePlugin();
|
||||
if (!plugin) {
|
||||
console.log('No supported DeckTape plugin detected, falling back to generic plugin');
|
||||
plugin = plugins['generic'].create(page, options);
|
||||
}
|
||||
} else {
|
||||
plugin = plugins[options.command].create(page, options);
|
||||
plugin = plugins[options.plugin].create(page, options);
|
||||
if (!plugin.isActive()) {
|
||||
console.log('Unable to activate the ' + plugin.getName() + ' DeckTape plugin for the address: ' + options.url);
|
||||
console.log('Unable to activate the ' + plugin.getName() + ' DeckTape plugin for the address: ' + options['URL']);
|
||||
phantom.exit(1);
|
||||
}
|
||||
}
|
||||
@ -173,7 +284,7 @@ function createActivePlugin() {
|
||||
}
|
||||
|
||||
function configure(plugin) {
|
||||
if (!options.size)
|
||||
if (!options['--size'])
|
||||
if (typeof plugin.size === 'function')
|
||||
options.size = plugin.size();
|
||||
else
|
||||
@ -185,7 +296,7 @@ function configure(plugin) {
|
||||
height: options.size.height + 'px',
|
||||
margin: '0px'
|
||||
};
|
||||
printer.outputFileName = options.filename;
|
||||
printer.outputFileName = options['FILE'];
|
||||
// TODO: ideally defined in the plugin prototype
|
||||
plugin.progressBarOverflow = 0;
|
||||
plugin.currentSlide = 1;
|
||||
@ -212,7 +323,7 @@ function exportSlide(plugin) {
|
||||
// TODO: support a more advanced "fragment to pause" mapping for special use cases like GIF animations
|
||||
// TODO: support plugin optional promise to wait until a particular mutation instead of a pause
|
||||
var decktape = Promise.resolve()
|
||||
.then(delay(options.pause))
|
||||
.then(delay(options['--pause']))
|
||||
.then(function () { system.stdout.write('\r' + progressBar(plugin)) })
|
||||
.then(function () { printer.printPage(page) });
|
||||
|
||||
@ -223,7 +334,7 @@ function exportSlide(plugin) {
|
||||
// e.g. for impress.js (may be needed to be configurable)
|
||||
.then(delay(1000))
|
||||
.then(function () {
|
||||
page.render(options.screenshotDirectory + '/' + options.filename.replace('.pdf', '_' + plugin.currentSlide + '_' + resolution.width + 'x' + resolution.height + '.' + options.screenshotFormat), { onlyViewport: true });
|
||||
page.render(options.screenshotDirectory + '/' + options['FILE'].replace('.pdf', '_' + plugin.currentSlide + '_' + resolution.width + 'x' + resolution.height + '.' + options.screenshotFormat), { onlyViewport: true });
|
||||
})
|
||||
}, decktape)
|
||||
.then(function () { page.viewportSize = options.size })
|
||||
|
1202
libs/docopt.js
Normal file
1202
libs/docopt.js
Normal file
File diff suppressed because it is too large
Load Diff
597
libs/nomnom.js
597
libs/nomnom.js
@ -1,597 +0,0 @@
|
||||
var _ = require("underscore"), chalk = require('chalk');
|
||||
|
||||
|
||||
function ArgParser() {
|
||||
this.commands = {}; // expected commands
|
||||
this.specs = {}; // option specifications
|
||||
}
|
||||
|
||||
ArgParser.prototype = {
|
||||
/* Add a command to the expected commands */
|
||||
command : function(name) {
|
||||
var command;
|
||||
if (name) {
|
||||
command = this.commands[name] = {
|
||||
name: name,
|
||||
specs: {}
|
||||
};
|
||||
}
|
||||
else {
|
||||
command = this.fallback = {
|
||||
specs: {}
|
||||
};
|
||||
}
|
||||
|
||||
// facilitates command('name').options().cb().help()
|
||||
var chain = {
|
||||
options : function(specs) {
|
||||
command.specs = specs;
|
||||
return chain;
|
||||
},
|
||||
opts : function(specs) {
|
||||
// old API
|
||||
return this.options(specs);
|
||||
},
|
||||
option : function(name, spec) {
|
||||
command.specs[name] = spec;
|
||||
return chain;
|
||||
},
|
||||
callback : function(cb) {
|
||||
command.cb = cb;
|
||||
return chain;
|
||||
},
|
||||
help : function(help) {
|
||||
command.help = help;
|
||||
return chain;
|
||||
},
|
||||
usage : function(usage) {
|
||||
command._usage = usage;
|
||||
return chain;
|
||||
}
|
||||
};
|
||||
return chain;
|
||||
},
|
||||
|
||||
nocommand : function() {
|
||||
return this.command();
|
||||
},
|
||||
|
||||
options : function(specs) {
|
||||
this.specs = specs;
|
||||
return this;
|
||||
},
|
||||
|
||||
opts : function(specs) {
|
||||
// old API
|
||||
return this.options(specs);
|
||||
},
|
||||
|
||||
globalOpts : function(specs) {
|
||||
// old API
|
||||
return this.options(specs);
|
||||
},
|
||||
|
||||
option : function(name, spec) {
|
||||
this.specs[name] = spec;
|
||||
return this;
|
||||
},
|
||||
|
||||
usage : function(usage) {
|
||||
this._usage = usage;
|
||||
return this;
|
||||
},
|
||||
|
||||
printer : function(print) {
|
||||
this.print = print;
|
||||
return this;
|
||||
},
|
||||
|
||||
script : function(script) {
|
||||
this._script = script;
|
||||
return this;
|
||||
},
|
||||
|
||||
scriptName : function(script) {
|
||||
// old API
|
||||
return this.script(script);
|
||||
},
|
||||
|
||||
help : function(help) {
|
||||
this._help = help;
|
||||
return this;
|
||||
},
|
||||
|
||||
colors: function() {
|
||||
// deprecated - colors are on by default now
|
||||
return this;
|
||||
},
|
||||
|
||||
nocolors : function() {
|
||||
this._nocolors = true;
|
||||
return this;
|
||||
},
|
||||
|
||||
parseArgs : function(argv) {
|
||||
// old API
|
||||
return this.parse(argv);
|
||||
},
|
||||
|
||||
nom : function(argv) {
|
||||
return this.parse(argv);
|
||||
},
|
||||
|
||||
parse : function(argv) {
|
||||
this.print = this.print || function(str, code) {
|
||||
console.log(str);
|
||||
process.exit(code || 0);
|
||||
};
|
||||
this._help = this._help || "";
|
||||
this._script = this._script || process.argv[0] + " "
|
||||
+ require('path').basename(process.argv[1]);
|
||||
this.specs = this.specs || {};
|
||||
|
||||
var argv = argv || process.argv.slice(2);
|
||||
|
||||
var arg = Arg(argv[0]).isValue && argv[0],
|
||||
command = arg && this.commands[arg],
|
||||
commandExpected = !_(this.commands).isEmpty();
|
||||
|
||||
if (commandExpected) {
|
||||
if (command) {
|
||||
_(this.specs).extend(command.specs);
|
||||
this._script += " " + command.name;
|
||||
if (command.help) {
|
||||
this._help = command.help;
|
||||
}
|
||||
this.specs.command = {
|
||||
hidden: true,
|
||||
name: 'command',
|
||||
position: 0,
|
||||
help: command.help
|
||||
};
|
||||
}
|
||||
else if (arg && !this.fallback) {
|
||||
return this.print(this._script + ": no such command '" + arg + "'", 1);
|
||||
}
|
||||
else {
|
||||
// no command but command expected e.g. 'git -v'
|
||||
var helpStringBuilder = {
|
||||
list : function() {
|
||||
return 'one of: ' + _(this.commands).keys().join(", ");
|
||||
},
|
||||
twoColumn : function() {
|
||||
// find the longest command name to ensure horizontal alignment
|
||||
var maxLength = _(this.commands).max(function (cmd) {
|
||||
return cmd.name.length;
|
||||
}).name.length;
|
||||
|
||||
// create the two column text strings
|
||||
var cmdHelp = _.map(this.commands, function(cmd, name) {
|
||||
var diff = maxLength - name.length;
|
||||
var pad = new Array(diff + 4).join(" ");
|
||||
return " " + [ name, pad, cmd.help ].join(" ");
|
||||
});
|
||||
return "\n" + cmdHelp.join("\n");
|
||||
}
|
||||
};
|
||||
|
||||
// if there are a small number of commands and all have help strings,
|
||||
// display them in a two column table; otherwise use the brief version.
|
||||
// The arbitrary choice of "20" comes from the number commands git
|
||||
// displays as "common commands"
|
||||
var helpType = 'list';
|
||||
if (_(this.commands).size() <= 20) {
|
||||
if (_(this.commands).every(function (cmd) { return cmd.help; })) {
|
||||
helpType = 'twoColumn';
|
||||
}
|
||||
}
|
||||
|
||||
this.specs.command = {
|
||||
name: 'command',
|
||||
position: 0,
|
||||
help: helpStringBuilder[helpType].call(this)
|
||||
};
|
||||
|
||||
if (this.fallback) {
|
||||
_(this.specs).extend(this.fallback.specs);
|
||||
this._help = this.fallback.help;
|
||||
} else {
|
||||
this.specs.command.required = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.specs.length === undefined) {
|
||||
// specs is a hash not an array
|
||||
this.specs = _(this.specs).map(function(opt, name) {
|
||||
opt.name = name;
|
||||
return opt;
|
||||
});
|
||||
}
|
||||
this.specs = this.specs.map(function(opt) {
|
||||
return Opt(opt);
|
||||
});
|
||||
|
||||
if (argv.indexOf("--help") >= 0 || argv.indexOf("-h") >= 0) {
|
||||
return this.print(this.getUsage());
|
||||
}
|
||||
|
||||
var options = {};
|
||||
var args = argv.map(function(arg) {
|
||||
return Arg(arg);
|
||||
})
|
||||
.concat(Arg());
|
||||
|
||||
var positionals = [];
|
||||
// preserves positional argument indexes
|
||||
if (!command && this.fallback)
|
||||
positionals.push(undefined);
|
||||
|
||||
/* parse the args */
|
||||
var that = this;
|
||||
args.reduce(function(arg, val) {
|
||||
/* positional */
|
||||
if (arg.isValue) {
|
||||
positionals.push(arg.value);
|
||||
}
|
||||
else if (arg.chars) {
|
||||
var last = arg.chars.pop();
|
||||
|
||||
/* -cfv */
|
||||
(arg.chars).forEach(function(ch) {
|
||||
that.setOption(options, ch, true);
|
||||
});
|
||||
|
||||
/* -v key */
|
||||
if (!that.opt(last).flag) {
|
||||
if (val.isValue) {
|
||||
that.setOption(options, last, val.value);
|
||||
return Arg(); // skip next turn - swallow arg
|
||||
}
|
||||
else {
|
||||
that.print("'-" + (that.opt(last).name || last) + "'"
|
||||
+ " expects a value\n\n" + that.getUsage(), 1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* -v */
|
||||
that.setOption(options, last, true);
|
||||
}
|
||||
|
||||
}
|
||||
else if (arg.full) {
|
||||
var value = arg.value;
|
||||
|
||||
/* --key */
|
||||
if (value === undefined) {
|
||||
/* --key value */
|
||||
if (!that.opt(arg.full).flag) {
|
||||
if (val.isValue) {
|
||||
that.setOption(options, arg.full, val.value);
|
||||
return Arg();
|
||||
}
|
||||
else {
|
||||
that.print("'--" + (that.opt(arg.full).name || arg.full) + "'"
|
||||
+ " expects a value\n\n" + that.getUsage(), 1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* --flag */
|
||||
value = true;
|
||||
}
|
||||
}
|
||||
that.setOption(options, arg.full, value);
|
||||
}
|
||||
return val;
|
||||
});
|
||||
|
||||
positionals.forEach(function(pos, index) {
|
||||
this.setOption(options, index, pos);
|
||||
}, this);
|
||||
|
||||
options._ = positionals;
|
||||
|
||||
this.specs.forEach(function(opt) {
|
||||
if (opt.default !== undefined && options[opt.name] === undefined) {
|
||||
this.setOption(options, opt.name, opt.default);
|
||||
}
|
||||
}, this);
|
||||
|
||||
// exit if required arg isn't present
|
||||
this.specs.forEach(function(opt) {
|
||||
if (opt.required && options[opt.name] === undefined) {
|
||||
var msg = opt.name + " argument is required";
|
||||
msg = this._nocolors ? msg : chalk.red(msg);
|
||||
|
||||
this.print("\n" + msg + "\n" + this.getUsage(), 1);
|
||||
}
|
||||
}, this);
|
||||
|
||||
if (command && command.cb) {
|
||||
command.cb(options);
|
||||
}
|
||||
else if (this.fallback && this.fallback.cb) {
|
||||
this.fallback.cb(options);
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
getUsage : function() {
|
||||
if (this.command && this.command._usage) {
|
||||
return this.command._usage;
|
||||
}
|
||||
else if (this.fallback && this.fallback._usage) {
|
||||
return this.fallback._usage;
|
||||
}
|
||||
if (this._usage) {
|
||||
return this._usage;
|
||||
}
|
||||
|
||||
// todo: use a template
|
||||
var str = "\n"
|
||||
if (!this._nocolors) {
|
||||
str += chalk.bold("Usage:");
|
||||
}
|
||||
else {
|
||||
str += "Usage:";
|
||||
}
|
||||
str += " " + this._script;
|
||||
|
||||
var positionals = _(this.specs).select(function(opt) {
|
||||
return opt.position != undefined;
|
||||
})
|
||||
positionals = _(positionals).sortBy(function(opt) {
|
||||
return opt.position;
|
||||
});
|
||||
var options = _(this.specs).select(function(opt) {
|
||||
return opt.position === undefined;
|
||||
});
|
||||
|
||||
if (options.length) {
|
||||
if (!this._nocolors) {
|
||||
// must be a better way to do this
|
||||
str += chalk.blue(" [options]");
|
||||
}
|
||||
else {
|
||||
str += " [options]";
|
||||
}
|
||||
}
|
||||
|
||||
// assume there are no gaps in the specified pos. args
|
||||
positionals.forEach(function(pos) {
|
||||
if (!pos.hidden) {
|
||||
str += " ";
|
||||
var posStr = pos.string;
|
||||
if (!posStr) {
|
||||
posStr = pos.name || "arg" + pos.position;
|
||||
if (pos.required) {
|
||||
posStr = "<" + posStr + ">";
|
||||
} else {
|
||||
posStr = "[" + posStr + "]";
|
||||
}
|
||||
if (pos.list) {
|
||||
posStr += "...";
|
||||
}
|
||||
}
|
||||
str += posStr;
|
||||
}
|
||||
});
|
||||
|
||||
if (options.length || positionals.length) {
|
||||
str += "\n\n";
|
||||
}
|
||||
|
||||
function spaces(length) {
|
||||
var spaces = "";
|
||||
for (var i = 0; i < length; i++) {
|
||||
spaces += " ";
|
||||
}
|
||||
return spaces;
|
||||
}
|
||||
var longest = positionals.reduce(function(max, pos) {
|
||||
return pos.name.length > max ? pos.name.length : max;
|
||||
}, 0);
|
||||
|
||||
positionals.forEach(function(pos) {
|
||||
if (!pos.hidden) {
|
||||
var posStr = pos.string || pos.name;
|
||||
str += posStr + spaces(longest - posStr.length) + " ";
|
||||
if (!this._nocolors) {
|
||||
str += chalk.grey(pos.help || "")
|
||||
}
|
||||
else {
|
||||
str += (pos.help || "")
|
||||
}
|
||||
str += "\n";
|
||||
}
|
||||
}, this);
|
||||
if (positionals.length && options.length) {
|
||||
str += "\n";
|
||||
}
|
||||
|
||||
if (options.length) {
|
||||
if (!this._nocolors) {
|
||||
str += chalk.blue("Options:");
|
||||
}
|
||||
else {
|
||||
str += "Options:";
|
||||
}
|
||||
str += "\n"
|
||||
|
||||
var longest = options.reduce(function(max, opt) {
|
||||
return opt.string.length > max && !opt.hidden ? opt.string.length : max;
|
||||
}, 0);
|
||||
|
||||
options.forEach(function(opt) {
|
||||
if (!opt.hidden) {
|
||||
str += " " + opt.string + spaces(longest - opt.string.length) + " ";
|
||||
|
||||
var defaults = (opt.default != null ? " [" + opt.default + "]" : "");
|
||||
var help = opt.help ? opt.help + defaults : "";
|
||||
str += this._nocolors ? help: chalk.grey(help);
|
||||
|
||||
str += "\n";
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
if (this._help) {
|
||||
str += "\n" + this._help;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
ArgParser.prototype.opt = function(arg) {
|
||||
// get the specified opt for this parsed arg
|
||||
var match = Opt({});
|
||||
this.specs.forEach(function(opt) {
|
||||
if (opt.matches(arg)) {
|
||||
match = opt;
|
||||
}
|
||||
});
|
||||
return match;
|
||||
};
|
||||
|
||||
ArgParser.prototype.setOption = function(options, arg, value) {
|
||||
var option = this.opt(arg);
|
||||
if (option.callback) {
|
||||
var message = option.callback(value);
|
||||
|
||||
if (typeof message == "string") {
|
||||
this.print(message, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (option.type != "string") {
|
||||
try {
|
||||
// infer type by JSON parsing the string
|
||||
value = JSON.parse(value)
|
||||
}
|
||||
catch(e) {}
|
||||
}
|
||||
|
||||
if (option.transform) {
|
||||
value = option.transform(value);
|
||||
}
|
||||
|
||||
var name = option.name || arg;
|
||||
if (option.choices && option.choices.indexOf(value) == -1) {
|
||||
this.print(name + " must be one of: " + option.choices.join(", "), 1);
|
||||
}
|
||||
|
||||
if (option.list) {
|
||||
if (!options[name]) {
|
||||
options[name] = [value];
|
||||
}
|
||||
else {
|
||||
options[name].push(value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
options[name] = value;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* an arg is an item that's actually parsed from the command line
|
||||
e.g. "-l", "log.txt", or "--logfile=log.txt" */
|
||||
var Arg = function(str) {
|
||||
var abbrRegex = /^\-(\w+?)$/,
|
||||
fullRegex = /^\-\-(no\-)?(.+?)(?:=(.+))?$/,
|
||||
valRegex = /^[^\-].*/;
|
||||
|
||||
var charMatch = abbrRegex.exec(str),
|
||||
chars = charMatch && charMatch[1].split("");
|
||||
|
||||
var fullMatch = fullRegex.exec(str),
|
||||
full = fullMatch && fullMatch[2];
|
||||
|
||||
var isValue = str !== undefined && (str === "" || valRegex.test(str));
|
||||
var value;
|
||||
if (isValue) {
|
||||
value = str;
|
||||
}
|
||||
else if (full) {
|
||||
value = fullMatch[1] ? false : fullMatch[3];
|
||||
}
|
||||
|
||||
return {
|
||||
str: str,
|
||||
chars: chars,
|
||||
full: full,
|
||||
value: value,
|
||||
isValue: isValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* an opt is what's specified by the user in opts hash */
|
||||
var Opt = function(opt) {
|
||||
var strings = (opt.string || "").split(","),
|
||||
abbr, full, metavar;
|
||||
for (var i = 0; i < strings.length; i++) {
|
||||
var string = strings[i].trim(),
|
||||
matches;
|
||||
if (matches = string.match(/^\-([^-])(?:\s+(.*))?$/)) {
|
||||
abbr = matches[1];
|
||||
metavar = matches[2];
|
||||
}
|
||||
else if (matches = string.match(/^\-\-(.+?)(?:[=\s]+(.+))?$/)) {
|
||||
full = matches[1];
|
||||
metavar = metavar || matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
matches = matches || [];
|
||||
var abbr = opt.abbr || abbr, // e.g. v from -v
|
||||
full = opt.full || full, // e.g. verbose from --verbose
|
||||
metavar = opt.metavar || metavar; // e.g. PATH from '--config=PATH'
|
||||
|
||||
var string;
|
||||
if (opt.string) {
|
||||
string = opt.string;
|
||||
}
|
||||
else if (opt.position === undefined) {
|
||||
string = "";
|
||||
if (abbr) {
|
||||
string += "-" + abbr;
|
||||
if (metavar)
|
||||
string += " " + metavar
|
||||
string += ", ";
|
||||
}
|
||||
string += "--" + (full || opt.name);
|
||||
if (metavar) {
|
||||
string += " " + metavar;
|
||||
}
|
||||
}
|
||||
|
||||
opt = _(opt).extend({
|
||||
name: opt.name || full || abbr,
|
||||
string: string,
|
||||
abbr: abbr,
|
||||
full: full,
|
||||
metavar: metavar,
|
||||
matches: function(arg) {
|
||||
return opt.full == arg || opt.abbr == arg || opt.position == arg
|
||||
|| opt.name == arg || (opt.list && arg >= opt.position);
|
||||
}
|
||||
});
|
||||
return opt;
|
||||
}
|
||||
|
||||
|
||||
var createParser = function() {
|
||||
return new ArgParser();
|
||||
}
|
||||
|
||||
var nomnom = createParser();
|
||||
|
||||
for (var i in nomnom) {
|
||||
if (typeof nomnom[i] == "function") {
|
||||
createParser[i] = _(nomnom[i]).bind(nomnom);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = createParser;
|
1343
libs/underscore.js
1343
libs/underscore.js
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,11 @@
|
||||
exports.help =
|
||||
'Requires the bespoke-extern module to expose the Bespoke.js API to a global variable named\n' +
|
||||
'\'bespoke\' and provides access to the collection of deck instances via \'bespoke.decks\'\n' +
|
||||
'and the most recent deck via \'bespoke.deck\'.';
|
||||
var chalk = require('chalk');
|
||||
|
||||
exports.cmd = [
|
||||
"Command: ",
|
||||
" Requires the 'bespoke-extern' module to expose the Bespoke.js API to a global ",
|
||||
" variable named 'bespoke'. The plugin provides access to the collection of deck",
|
||||
" instances via 'bespoke.decks' and the most recent deck via 'bespoke.deck'. "
|
||||
];
|
||||
|
||||
exports.create = function (page) {
|
||||
return new Bespoke(page);
|
||||
|
@ -1,23 +1,30 @@
|
||||
// The generic plugin emulates end-user interaction by pressing keyboard and detects changes to the DOM.
|
||||
// The deck is considered over when no change is detected afterward.
|
||||
var chalk = require('chalk');
|
||||
|
||||
exports.options = {
|
||||
keycode: {
|
||||
default: 'Right',
|
||||
help: 'Key code pressed to navigate to next slide'
|
||||
},
|
||||
maxSlides: {
|
||||
help: 'Maximum number of slides to export'
|
||||
}
|
||||
};
|
||||
exports.cmd = [
|
||||
' ',
|
||||
'Usage: ',
|
||||
' decktape.js [options] [--keycode=CODE --maxSlides=N] generic URL FILE ',
|
||||
' ',
|
||||
'Options: ',
|
||||
' --keycode=CODE Key code pressed to navigate [default: Right]',
|
||||
' to the next slide ',
|
||||
' --maxSlides=N Maximum number of slides to ',
|
||||
' export ',
|
||||
' ',
|
||||
'Command: ',
|
||||
' Emulates the end-user interaction by pressing the key with the specified ',
|
||||
' keycode option and iterates over the presentation as long as: ',
|
||||
' - Any change to the DOM is detected by observing mutation events targeting the',
|
||||
' body element and its subtree or, ',
|
||||
' - the number of slides exported has reached the specified maxSlides option. ',
|
||||
' ',
|
||||
' The keycode option must be one of the PhantomJS page event keys and defaults ',
|
||||
' to [Right]. '
|
||||
];
|
||||
|
||||
exports.help =
|
||||
'Emulates the end-user interaction by pressing the key with the specified [keycode] option\n' +
|
||||
'and iterates over the presentation as long as:\n' +
|
||||
'- Any change to the DOM is detected by observing mutation events targeting the body element\n' +
|
||||
' and its subtree,\n' +
|
||||
'- Nor the number of slides exported has reached the specified [maxSlides] option.\n' +
|
||||
'The [keycode] option must be one of the PhantomJS page event keys and defaults to [Right].';
|
||||
exports.help = [
|
||||
{ regex: /(keycode(?!=)|maxSlides(?!=))/g, style: chalk.underline }
|
||||
];
|
||||
|
||||
exports.create = function (page, options) {
|
||||
return new Generic(page, options);
|
||||
@ -27,7 +34,7 @@ function Generic(page, options) {
|
||||
this.page = page;
|
||||
this.options = options;
|
||||
this.isNextSlideDetected = false;
|
||||
this.keycode = this.page.event.key[this.options.keycode || exports.options.keycode.default];
|
||||
this.keycode = this.page.event.key[this.options['--keycode']];
|
||||
}
|
||||
|
||||
Generic.prototype = {
|
||||
@ -45,7 +52,9 @@ Generic.prototype = {
|
||||
var observer = new window.MutationObserver(function () {
|
||||
window.callPhantom({ isNextSlideDetected: true });
|
||||
});
|
||||
observer.observe(document.querySelector('body'), { attributes: true, childList: true, subtree: true });
|
||||
observer.observe(document.querySelector('body'), {
|
||||
attributes: true, childList: true, subtree: true
|
||||
});
|
||||
});
|
||||
var plugin = this;
|
||||
this.page.onCallback = function (mutation) {
|
||||
@ -58,18 +67,24 @@ Generic.prototype = {
|
||||
return undefined;
|
||||
},
|
||||
|
||||
// A priori knowledge is impossible to achieve in a generic way. Thus the only way is to actually emulate end-user interaction by pressing the configured key and check whether the DOM has changed a posteriori.
|
||||
// A priori knowledge is impossible to achieve in a generic way. Thus the only
|
||||
// way is to actually emulate end-user interaction by pressing the configured key
|
||||
// and check whether the DOM has changed a posteriori.
|
||||
hasNextSlide: function () {
|
||||
if (this.options.maxSlides && this.currentSlide >= this.options.maxSlides)
|
||||
if (this.options['maxSlides'] && this.currentSlide >= this.options['maxSlides'])
|
||||
return false;
|
||||
// PhantomJS actually sends a 'keydown' DOM event when sending a 'keypress' user event. Hence 'keypress' event is skipped to avoid moving forward two steps instead of one. See https://github.com/ariya/phantomjs/issues/11094 for more details.
|
||||
// PhantomJS actually sends a 'keydown' DOM event when sending a 'keypress'
|
||||
// user event. Hence 'keypress' event is skipped to avoid moving forward
|
||||
// two steps instead of one. See https://github.com/ariya/phantomjs/issues/11094
|
||||
// for more details.
|
||||
['keydown'/*, 'keypress'*/, 'keyup'].forEach(function (event) {
|
||||
this.page.sendEvent(event, this.keycode);
|
||||
}, this);
|
||||
var plugin = this;
|
||||
return new Promise(function (fulfill) {
|
||||
// TODO: use mutation event directly instead of relying on a timeout
|
||||
// TODO: detect cycle to avoid infinite navigation for frameworks that support loopable presentations like impress.js and flowtime.js
|
||||
// TODO: detect cycle to avoid infinite navigation for frameworks
|
||||
// that support loopable presentations like impress.js and flowtime.js
|
||||
setTimeout(function () {
|
||||
fulfill(plugin.isNextSlideDetected);
|
||||
}, 1000);
|
||||
|
Loading…
Reference in New Issue
Block a user