Use Docopt for CLI definition and parsing

This commit is contained in:
Antonin Stefanutti 2016-03-23 11:23:59 +01:00
parent 27890ea897
commit e45d4bdd91
6 changed files with 1449 additions and 2057 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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);

View File

@ -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);