Home Reference Source Test

src/Help.js

import pad from 'pad';
import wordwrap from 'wordwrap';
import {log, colors} from 'gulp-util';
import Task from './Task';
import Argument from  './Argument';

const MIN_NAME_LENGTH = 15;
const MAX_LINE_LENGTH = 80;
const COL_PADDING = 2;
const lineWrap = wordwrap(MAX_LINE_LENGTH);

function logLines(line, color) {
	let lines = lineWrap(line).split('\n');
	lines.forEach((l) => {
		log(color(l));
	});
}

function wrapTask(task) {
	return colors.yellow(task);
}

function wrapArgument(arg) {
	return colors.magenta.bold(arg);
}

/**
 * Describes a series of tasks, their dependencies and arguments
 */
export default class Help {

	/**
	 * @param {string} [title] The title of the project
	 * @param {string} [description] A description for the project
     */
	constructor(title, description) {
		this._title = title;
		this._description = description;
		this._tasks = {};
		this._arguments = {};
		this._maxLabelLength = MIN_NAME_LENGTH;
	}

	/**
	 * Register a task
	 * @param {string} taskName The name
	 * @param {string} description The description
	 * @param {Task[]} [dependencies=[]] The dependant tasks
     * @param {Argument[]} [args=[]] The arguments
     */
	registerTask(taskName, description, dependencies = [], args = []) {
		const task = new Task(taskName, description);

		dependencies.forEach((dep) => {
			// validate dependency
			if (!this._tasks[dep]) {
				throw new Error('The dependency, ' + dep + ', is not registered');
			}
			task.addDependency(this._tasks[dep]);
		});

		args.forEach((arg) => {
			// validate arguments
			if (!this._arguments[arg]) {
				throw new Error('The argument, ' + arg + ', is not registered');
			}
			task.addArgument(this._arguments[arg]);
		});

		this._tasks[taskName] = task;

		if (taskName.length > this._maxNameLength) {
			this._maxNameLength = taskName.length;
		}
	}

	/**
	 * Register an argument
	 *
	 * @param {string} argumentName The name
	 * @param {string} description The description
	 * @param {string} [defaultValue] The default value
     */
	registerArgument(argumentName, description, defaultValue) {
		this._arguments[argumentName] = new Argument(argumentName, description, defaultValue);

		if (argumentName.length > this._maxLabelLength) {
			this._maxLabelLength = argumentName.length;
		}
	}

	/**
	 * Get a gulp task function. This should be passed to gulp.task.
	 * @returns {function(): undefined} The gulp task function
     */
	get helpTask() {
		return () => {
			this._printHeader();
			this._printTaskList();
			this._printArgumentsList();
			this._printTaskDetails();
		};
	}

	_printHeader() {
		log();
		if (this._title) {
			log(colors.cyan.bold(this._title));
		}
		log(colors.gray('usage: ') + colors.blue('gulp ') + '<' + wrapTask('task') + '> <' + wrapArgument('arguments') + '>');
		if (this._description) {
			log();
			logLines(this._description, colors.white);
		}
	}

	_printTaskList() {
		const taskColWidth = this._getMaxTaskNameLength() + COL_PADDING;
		const taskNames = this._getTaskNamesList();
		const numberOfTaskColumns = Math.floor(MAX_LINE_LENGTH / (taskColWidth));
		let line;

		if (!taskNames.length) {
			return;
		}

		log();
		log(colors.green.underline('Tasks List'));
		line = '';
		for (let i = 1; i <= taskNames.length; i++) {
			line += wrapTask(pad(taskNames[i - 1], taskColWidth));
			if ((i % numberOfTaskColumns) === 0) {
				log(line);
				line = '';
			}
		}
		// log last line if it exists
		if (line !== '') {
			log(line);
		}
	}

	_printArgumentsList() {
		const argsColWidth = this._getMaxArgumentNameLength() + COL_PADDING + 2; // +2 for --
		const args = this._getArgumentsList();
		const max_line_length = MAX_LINE_LENGTH - argsColWidth;
		const indent = ' '.repeat(argsColWidth);
		let wrap = wordwrap(max_line_length);

		if (!args.length) {
			return;
		}

		log();
		log(colors.green.underline('Arguments List'));
		args.forEach((arg) => {
			let argumentName = pad('--' + arg.name, argsColWidth);

			if (arg.description) {
				let lines = wrap(arg.description).split("\n");
				log(wrapArgument(argumentName) + colors.white(lines[0]));
				
				// rest of the lines, besides last
				for (let i = 1; i < lines.length ; i++) {
					log(indent + colors.white(lines[i]));
				}
			}
			else {
				log(wrapArgument(argumentName));
			}

			// print defaults
			if (arg.defaultValue) {
				log(indent + colors.cyan('Default: ') + colors.yellow(arg.defaultValue));
			}
			log();
		});
	}

	_printTaskDetails() {
		const tasks = this._getTasksList();
		const taskColWidth = this._getMaxTaskNameLength() + COL_PADDING;
		const maxLineLength = MAX_LINE_LENGTH - taskColWidth;
		const indent = ' '.repeat(taskColWidth);
		let wrap = wordwrap(maxLineLength);

		if (!tasks.length) {
			return;
		}

		log();
		log(colors.green.underline('Tasks Details'));
		tasks.forEach((task) => {
			let taskName = pad(task.name, taskColWidth);
			let lines = wrap(task.description).split("\n");

			log(wrapTask(taskName) + colors.white(lines[0]));

			// rest of the lines
			for (let i = 1; i < lines.length ; i++) {
				log(indent + colors.white(lines[i]));
			}

			Help._printTaskArguments(indent, maxLineLength, task.argumentsNames);
			Help._printTaskDependencies(indent, maxLineLength, task.dependenciesNames);

			log();
		});

	}

	_getTasksList() {
		// turn tasks object into a task name sorted array
		return Object.keys(this._tasks).sort().map((task) => {
			return this._tasks[task];
		});
	}

	_getTaskNamesList() {
		// turn tasks object into a task name sorted array
		return Object.keys(this._tasks).sort();
	}

	_getMaxTaskNameLength() {
		return Object.keys(this._tasks).reduce((prevValue, currentValue) => {
			return (prevValue > currentValue.length ? prevValue : currentValue.length);
		}, 0);
	}

	_getArgumentsList() {
		// turn arguments object into a argument name sorted array
		return Object.keys(this._arguments).sort().map((arg) => {
			return this._arguments[arg];
		});
	}

	_getArgumentsNamesList() {
		// turn tasks object into a task name sorted array
		return Object.keys(this._arguments).sort();
	}

	_getMaxArgumentNameLength() {
		return Object.keys(this._arguments).reduce((prevValue, currentValue) => {
			return (prevValue > currentValue.length ? prevValue : currentValue.length);
		}, 0);
	}

	static _printTaskDependencies(indent, maxLineLength, dependencies) {
		let out;
		let length;
		const baseLength = 11 + indent.length;

		// do nothing on empty dependencies
		if (!dependencies.length) {
			return;
		}

		out = colors.green.dim('Sub Tasks: ');
		length = baseLength; // Sub Tasks length before wrap
		for (let i = 0; i < dependencies.length; i++) {
			let dep = dependencies[i];
			let nameLength = dep.length; // +2 for space and comma

			// add trailing comma to all but last
			if (i < dependencies.length - 1) {
				nameLength += 2;
			}

			if (length + nameLength > maxLineLength) {
				log(indent + out);
				out = '           ';
				length = baseLength;
			}
			out += wrapTask(dep);

			// add trailing comma to all but last
			if (i < dependencies.length - 1) {
				out += ', ';
			}

			length += nameLength;
		}
		log(indent + out);
	}

	static _printTaskArguments(indent, maxLineLength, args) {
		let out;
		let length;
		const baseLength = 11 + indent.length;

		// do nothing on empty dependencies
		if (!args.length) {
			return;
		}

		out = colors.green.dim('Arguments: ');
		length = baseLength; // Sub Tasks length before wrap
		for (let i = 0; i < args.length; i++) {
			let arg = args[i];
			let nameLength = arg.length; // +2 for space and comma

			// add trailing comma length to all but last
			if (i < args.length - 1) {
				length += 2;
			}

			if (length + nameLength > maxLineLength) {
				log(indent + out);
				out = '           ';
				length = baseLength;
			}
			out += wrapArgument(arg);

			// add trailing comma to all but last
			if (i < args.length - 1) {
				out += ', ';
			}

			length += nameLength;
		}
		log(indent + out);
	}

}