UNPKG

trainingproject

Version:

A module to teach you how to module.

567 lines (476 loc) 16.3 kB
const path = require('path') const commandico = require('commandico') const inherits = require('util').inherits const EventEmitter = require('events').EventEmitter const util = require('./util') const PrintStream = require('./lib/print') const storage = require('workshopper-adventure-storage') function WA (options) { if (!(this instanceof WA)) { return new WA(options) } if (!options) { options = {} } if (options.appDir) { options.appDir = util.getDir(options.appDir, '.') if (!options.name) { try { options.name = require(path.join(options.appDir, 'package.json')).name } catch (e) {} } } if (!options.name) { throw new Error('The workshopper needs a name to store the progress.') } if (!options.languages) { options.languages = ['en'] } if (!options.defaultLang) { options.defaultLang = options.languages[0] } if (!options.defaultOutputType) { options.defaultOutputType = 'md' } if (!options.pkg && options.appDir) { try { options.pkg = require(path.join(options.appDir, 'package.json')) } catch (e) {} } if (!options.appRepo && options.pkg) { options.appRepo = options.pkg.repository.url } if (!options.version && options.pkg) { options.version = options.pkg.version } if (options.appDir) { options.exerciseDir = util.getDir(options.exerciseDir || 'exercises', options.appDir) } if (!options.menu) { options.menu = { width: 73, x: 2, y: 2 } } if (options.requireSubmission === undefined) { options.requireSubmission = false } if (!options.menuFactory) { options.menuFactory = require('simple-terminal-menu/factory')(options.menu, {}) } EventEmitter.call(this) this.options = options var globalStorage = storage(storage.userDir, '.config', 'workshopper') this.appStorage = storage(storage.userDir, '.config', options.name) this.exercises = [] this._meta = {} try { this.i18n = require('./i18n').init(options, globalStorage, this.appStorage) } catch (e) { console.log(e.message) process.exit(1) } this.__ = this.i18n.__ this.__n = this.i18n.__n this.cli = commandico(this, 'menu') .loadCommands(path.resolve(__dirname, './lib/commands')) .loadModifiers(path.resolve(__dirname, './lib/modifiers')) if (options.commands) { this.cli.addCommands(options.commands) } if (options.modifiers) { this.cli.addModifiers(options.modifiers) } } inherits(WA, EventEmitter) WA.prototype.execute = function (args) { return this.cli.execute(args) } WA.prototype.add = function (nameOrObject, fnOrObject, fn) { var meta try { meta = require('./lib/createExerciseMeta')(this.options.exerciseDir, nameOrObject, fnOrObject, fn) } catch (e) { console.log(e) return new Error(this.__('error.exercise.' + e.id, e)) } return this.addExercise(meta) } WA.prototype.addAll = function (list) { return list.map(this.add.bind(this)) } WA.prototype.addExercise = function (meta) { this.exercises.push(meta.name) this._meta[meta.id] = meta meta.number = this.exercises.length return this } WA.prototype.getVersionString = function () { return this.options.name + '@' + this.options.version } WA.prototype.countRemaining = function () { var completed = this.appStorage.get('completed') return this.exercises.length - (completed ? completed.length : 0) } WA.prototype.markCompleted = function (exerciseName, cb) { var completed = this.appStorage.get('completed') || [] if (completed.indexOf(exerciseName) === -1) { completed.push(exerciseName) } this.appStorage.save('completed', completed) if (this.onComplete.length === 0) { throw new Error('The workshoppers `.onComplete` method must have at least one callback argument') } return this.onComplete(cb) } WA.prototype.getNext = function () { var current = this.appStorage.get('current') var remainingAfterCurrent = this.exercises.slice(this.exercises.indexOf(current) + 1) var completed = this.appStorage.get('completed') || [] var incompleteAfterCurrent = remainingAfterCurrent.filter(function (elem) { return completed.indexOf(elem) < 0 }) if (incompleteAfterCurrent.length === 0) { return new Error('error.no_uncomplete_left') } return incompleteAfterCurrent[0] } WA.prototype.onComplete = function (cb) { setImmediate(cb) } // overall exercise fail WA.prototype.exerciseFail = function (mode, exercise, stream, cb) { ( stream.append(exercise.fail, exercise.failType || this.options.defaultOutputType) || stream.append(this.options.fail, this.options.failType || this.options.defaultOutputType) ) && stream.append('\n') cb() } WA.prototype.getExerciseFiles = function (exercise, callback) { if (!exercise.hideSolutions && typeof exercise.getSolutionFiles === 'function') { return exercise.getSolutionFiles(callback) } setImmediate(callback.bind(null, null, exercise.solutionFiles || [])) } // overall exercise pass WA.prototype.exercisePass = function (mode, exercise, stream, cb) { this.getExerciseFiles(exercise, function (err, files) { if (err) { return cb(this.__('solution.notes.load_error', {err: err.message || err}), false, stream) } this.markCompleted(exercise.meta.name, function (err, completeMessage) { if (err) { return cb(err, false) } var appended = stream.append(completeMessage, this.options.defaultOutputType) || stream.append(exercise.pass, exercise.passType || this.options.defaultOutputType) || stream.append(this.options.pass, this.options.passType || this.options.defaultPassType) var hideSolutions = exercise.hideSolutions if (hideSolutions === undefined) { hideSolutions = this.options.hideSolutions } if (hideSolutions !== true) { if ((files && files.length > 0) || exercise.solution) { stream.append('{solution.notes.compare}') } files && files.length > 0 ? stream.append({ files: files }) : stream.append(exercise.solution, exercise.solutionType || this.options.defaultSolutionType) appended = true } var hideRemaining = exercise.hideRemaining if (hideRemaining === undefined) { hideRemaining = this.options.hideRemaining } if (hideRemaining !== true) { var remaining = this.countRemaining() remaining > 0 ? stream.append( '{progress.remaining#' + remaining + '}\n\n' + '{ui.return}\n') : stream.append('{progress.finished}\n') appended = true } if (appended) { stream.append('\n') } cb(null, true) }.bind(this)) }.bind(this)) } WA.prototype.verify = function (args, specifier, contentOnly, cb) { return this.process('verify', args, specifier, contentOnly, cb) } WA.prototype.run = function (args, specifier, cb) { return this.process('run', args, specifier, cb) } WA.prototype.process = function (mode, args, specifier, contentOnly, cb) { var exercise = this.loadExercise(specifier) var stream = this.createMarkdownStream(exercise) if (!cb && typeof contentOnly === 'function') { cb = contentOnly contentOnly = false } var _cb = cb cb = function (err, pass) { setImmediate(function () { _cb(err, pass) }) } if (!exercise) { cb(this.__('error.exercise.missing', {name: specifier}), false) return stream } var requireSubmission = exercise.requireSubmission if (requireSubmission === undefined) { requireSubmission = this.options.requireSubmission } if (requireSubmission !== false && args.length === 0) { cb(this.__('ui.usage', {appName: this.options.name, mode: mode}), false, stream) return stream } var method = exercise[mode] if (!method) { cb(this.__('error.exercise.method_not_required', {method: mode}), false, stream) return stream } if (typeof method !== 'function') { cb('The `.' + mode + '` object of the exercise `' + exercise.meta.id + ' is a `' + typeof method + '`. It should be a `function` instead.', false, stream) return stream } stream.startWaiting() stream = this.executeExercise(exercise, mode, method, args, stream, contentOnly, cb) if (typeof exercise.on === 'function') { exercise.on('pass', function (message) { stream.append({ text: require('chalk').green.bold('\u2713 '), type: (message && message.type) || 'md', skipNewline: true }) stream.append(message, this.options.defaultOutputType) }.bind(this)) exercise.on('fail', function (message) { stream.append({ text: require('chalk').red.bold('\u2717 '), type: (message && message.type) || 'md', skipNewline: true }) stream.append(message, this.options.defaultOutputType) }.bind(this)) exercise.on('pass', this.emit.bind(this, 'pass', exercise, mode)) exercise.on('fail', this.emit.bind(this, 'fail', exercise, mode)) } return stream } WA.prototype.executeExercise = function (exercise, mode, method, args, stream, contentOnly, cb) { if (!cb && typeof contentOnly === 'function') { cb = contentOnly contentOnly = false } if (!contentOnly && mode === 'verify') { (stream.append(exercise.header, this.options.defaultOutputType) || stream.append(this.options.header, this.options.defaultOutputType)) } var result var finished = false var cleanup = function cleanup (err, pass, message, messageType) { if (finished) { return // TODO: make this easier to debug ... bad case of zalgo } finished = true if (message) { if (typeof message === 'string') { message = { text: message, type: messageType || this.options.defaultOutputType } } if (pass) { exercise.pass = [ exercise.pass || this.options.pass, message ] } else { exercise.fail = [ exercise.fail || this.options.fail, message ] } } if (err) { return cb(this.__('error.exercise.unexpected_error', { mode: mode, err: (err.stack || err) }), false) } var writeFooter = function () { if (!contentOnly && mode === 'verify') { // TODO: Make this footer great again once we fixed workshopper-exercise if (stream.append(exercise.footer, this.options.defaultOutputType) || stream.append(this.options.footer, this.options.defaultOutputType)) { stream.append('\n') } return true } return false }.bind(this) var end = function (err) { if (typeof exercise.end !== 'function') { writeFooter() return cb(null, pass) } exercise.end(mode, pass, function (cleanupErr) { if (cleanupErr) { return cb(this.__('error.cleanup', {err: cleanupErr.message || cleanupErr}), false) } writeFooter() stream.stopWaiting() // to end this farce cb(err, pass) }.bind(this)) }.bind(this) if (mode === 'run') { return setImmediate(end) } if (pass) { this.exercisePass(mode, exercise, stream, end) } else { this.exerciseFail(mode, exercise, stream, end) } }.bind(this) try { result = method.length <= 1 ? cleanup(null, true, method.call(exercise, args)) : method.call(exercise, args, function callback (err, pass, message) { /* err ... Error that occured pass ... true = The run has worked message ... message to Append after the output callback(true) -> err=null, pass=true callback(false) -> err=null, pass=false callback() -> err=null, pass=null callback(null) -> err=null, pass=null callback(true, true) -> err=true, pass="x" callback(false, "x") -> err=false, pass="x" callback(null, "x") -> err=null, pass="x" callback("x", false) -> err="x", pass=false callback("x", true) -> err="x", pass=true ... pass should be ignored */ if (pass === undefined && (err === true || err === false || err === undefined || err === null)) { pass = err err = null } pass = (mode === 'run' || (pass && !exercise.fail)) err ? cleanup(err, null, message) : cleanup(null, pass, message) }) } catch (e) { cleanup(e, false) return stream } return this.processResult(result, stream) } WA.prototype.processResult = function (result, stream) { stream.append(result, this.options.defaultOutputType) return stream } WA.prototype.loadExercise = function (specifier) { var id if (specifier) { id = this.specifierToId(specifier) } else { id = util.idFromName(this.appStorage.get('current')) } if (!id) { return null } var meta = this._meta[id] if (!meta) { return null } var exercise = meta.fn() exercise.meta = meta if (typeof exercise.init === 'function') { exercise.init(this, meta.id, meta.name, meta.dir, meta.number) } return exercise } WA.prototype.specifierToId = function (specifier) { if (!isNaN(specifier)) { var number = parseInt(specifier, 10) if (number >= 0 && number < this.exercises.length) { specifier = this.exercises[number] } else { specifier = '' } } return util.idFromName(specifier) } WA.prototype.selectExercise = function (specifier) { var id = this.specifierToId(specifier) if (!id) { throw new Error(this.__('error.exercise.missing', {name: specifier})) } var meta = this._meta[id] if (!meta) { throw new Error(this.__('error.exercise.missing', {name: specifier})) } this.appStorage.save('current', meta.name) return meta.id } WA.prototype.createMarkdownStream = function (exercise) { var context = exercise ? this.createExerciseContext(exercise) : this.i18n return new PrintStream(context, this.i18n.lang()) } WA.prototype.createExerciseContext = function (exercise) { return this.i18n.extend({ 'currentExercise.name': this.__('exercise.' + exercise.meta.name), 'progress.count': exercise.meta.number, 'progress.total': this.exercises.length, 'progress.state_resolved': this.__('progress.state', {count: exercise.meta.number, amount: this.exercises.length}) }) } WA.prototype.getExerciseText = function printExercise (specifier, contentOnly, callback) { var exercise = this.loadExercise(specifier) var prepare if (arguments.length === 2) { callback = contentOnly contentOnly = false } if (!exercise) { callback(this.__('error.exercise.none_active')) } prepare = (typeof exercise.prepare === 'function') ? exercise.prepare.bind(exercise) : setImmediate prepare(function (err) { if (err) { return callback(this.__('error.exercise.preparing', {err: err.message || err})) } var getExerciseText = (typeof exercise.getExerciseText === 'function') ? exercise.getExerciseText.bind(exercise) : setImmediate getExerciseText(function (err, exerciseTextType, exerciseText) { if (err) { return callback(this.__('error.exercise.loading', {err: err.message || err})) } var stream = this.createMarkdownStream(exercise) var found = false if (!contentOnly) { stream.append(exercise.header, this.options.defaultOutputType) || stream.append(this.options.header, this.options.defaultOutputType) } if (stream.append(exercise.problem, exercise.problemType || this.options.defaultOutputType)) { found = true } if (stream.append(exerciseText, exerciseTextType || this.options.defaultOutputType)) { found = true } if (!found) { return callback('The exercise "' + exercise.meta.name + '" is missing a problem definition!') } if (!contentOnly) { stream.append(exercise.footer, this.options.defaultOutputType) || stream.append(this.options.footer, this.options.defaultOutputType) && stream.append('\n') } callback(null, stream) }.bind(this)) }.bind(this)) } module.exports = WA