From 7af50c51f604d41c6dda43d9e16c0fde76ce2c47 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Thu, 14 Jan 2016 08:48:07 -0500 Subject: [PATCH] Gather. --- README.md | 34 ++++++++- src/cli/error-handler.js | 137 +++++++++++++++++++------------------ src/cli/main.js | 42 +++++++++--- src/cli/out.js | 80 +++++++++++++--------- src/core/resume-factory.js | 16 +++-- src/utils/md2chalk.js | 18 +++++ src/verbs/build.js | 15 ++-- src/verbs/verb.js | 37 +++++++++- test/test-stdout.js | 7 +- 9 files changed, 262 insertions(+), 124 deletions(-) create mode 100644 src/utils/md2chalk.js diff --git a/README.md b/README.md index c28a0ba..d6c23e7 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,36 @@ hackmyresume BUILD me.json TO out/resume.all `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`. +### Building PDFs + +HackMyResume takes a unique approach to PDF generation. Instead of enforcing +a specific PDF engine on users, HackMyResume will attempt to work with whatever +PDF engine you have installed through the engine's command-line interface (CLI). +Currently that means one or both of... + +- [wkhtmltopdf][3] +- [Phantom.js][3] + +..with support for other engines planned in the future. **One or both of these +engines must be installed and accessible on your PATH in order to generate PDF +resumes with HackMyResume**. That means you should be able to invoke either of +these tools directly from your shell or terminal without error: + +```bash +wkhtmltopdf input.html output.pdf +phantomjs script.js input.html output.pdf +``` + +Assuming you've installed one or both of these engines on your system, you can +tell HackMyResume which flavor of PDF generation to use via the `--pdf` option +(`-p` for short): + +```bash +hackmyresume BUILD resume.json TO out.all --pdf phantom +hackmyresume BUILD resume.json TO out.all --pdf wkhtmltopdf +hackmyresume BUILD resume.json TO out.all --pdf none +``` + ### Analyzing HackMyResume can analyze your resume for keywords, employment gaps, and other @@ -292,7 +322,7 @@ Depending on the HackMyResume version, you should see output similar to: ``` -*** HackMyResume v1.4.1 *** +*** HackMyResume v1.6.0 *** Reading resume: resume.json Analyzing FRESH resume: resume.json @@ -388,7 +418,7 @@ hackmyresume VALIDATE resumeA.json resumeB.json HackMyResume will validate each specified resume in turn: ```bash -*** HackMyResume v0.9.0 *** +*** HackMyResume v1.6.0 *** Validating JSON resume: resumeA.json (INVALID) Validating JSON resume: resumeB.json (VALID) ``` diff --git a/src/cli/error-handler.js b/src/cli/error-handler.js index 42cdeee..7a865dc 100644 --- a/src/cli/error-handler.js +++ b/src/cli/error-handler.js @@ -18,89 +18,90 @@ Error-handling routines for HackMyResume. , WRAP = require('word-wrap') , chalk = require('chalk') , SyntaxErrorEx = require('../utils/syntax-error-ex'); + require('string.prototype.startswith'); /** - An amorphous blob of error handling code for HackMyResume. + Error handler for HackMyResume. All errors are handled here. @class ErrorHandler */ var ErrorHandler = module.exports = { init: function( debug ) { this.debug = debug; + return this; }, err: function( ex, shouldExit ) { - var msg = '', exitCode, log = console.log, showStack = ex.showStack; + if( ex.fluenterror ) { - // If the exception has been handled elsewhere and shouldExit is true, - // let's get out of here, otherwise silently return. - if( ex.handled ) { - if( shouldExit ) - process.exit( exitCode ); - return; - } + var objError = assembleError( ex ); + o( this[ 'format' + (objError.warning ? 'Warning' : 'Error')]( objError.msg ) ); - // Get an error message -- either a HackMyResume error message or the - // exception's associated error message - if( ex.fluenterror ){ - var errInfo = get_error_msg( ex ); - msg = errInfo.msg; - exitCode = ex.fluenterror; - showStack = errInfo.showStack; + if( objError.showStack ) + o( chalk.red( ex.stack || ex.inner.stack ) ); + + if( objError.quit ) { + this.debug && o( + chalk.cyan('Exiting with error code ' + ex.fluenterror.toString())); + process.exit( ex.fluenterror ); + } } else { - msg = ex.toString(); - exitCode = -1; - // Deal with pesky 'Error:' prefix. - var idx = msg.indexOf('Error: '); - msg = idx === -1 ? msg : msg.substring( idx + 7 ); + o( ex ); + var stackTrace = ex.stack || (ex.inner && ex.inner.stack) + if( stackTrace && this.debug ) + o( ex.stack || ex.inner.stack ); + + // if( this.debug ) + // o( ex.stack || ex.inner.stack ); } - // Log non-HackMyResume-handled errors in red with ERROR prefix. Log HMR - // errors as-is. - ex.fluenterror ? - log( msg.toString() ) : - log( chalk.red.bold('ERROR: ' + msg.toString()) ); + }, - // Selectively show the stack trace - if( (ex.stack || (ex.inner && ex.inner.stack)) && - ((showStack && ex.code !== 'ENOENT' ) || (this.debug) )) - log( chalk.red( ex.stack || ex.inner.stack ) ); + formatError: function( msg ) { + return chalk.red.bold( + msg.toUpperCase().startsWith('ERROR:') ? msg : 'Error: ' + msg ); + }, - // Let the error code be the process's return code. - ( shouldExit || ex.shouldExit ) && process.exit( exitCode ); + formatWarning: function( brief, msg ) { + return chalk.yellow(brief) + chalk.yellow(msg || ''); } }; + var o = function() { + console.log.apply( console.log, arguments ); + }; - function get_error_msg( ex ) { + function assembleError( ex ) { + + var msg = '', withStack = false, isError = false, quit = true, warn = true; - var msg = '', withStack = false, isError = false; switch( ex.fluenterror ) { case HACKMYSTATUS.themeNotFound: - msg = formatWarning( + msg = chalk.bold("Couldn't find the '" + ex.data + "' theme."), " Please specify the name of a preinstalled FRESH theme " + - "or the path to a locally installed FRESH or JSON Resume theme."); + "or the path to a locally installed FRESH or JSON Resume theme."; break; case HACKMYSTATUS.copyCSS: - msg = formatWarning("Couldn't copy CSS file to destination folder."); + msg = "Couldn't copy CSS file to destination folder."; + quit = false; break; case HACKMYSTATUS.resumeNotFound: - msg = formatWarning('Please ' + chalk.bold('feed me a resume') + - ' in FRESH or JSON Resume format.'); + msg = 'Please ' + chalk.bold('feed me a resume') + + ' in FRESH or JSON Resume format.'; break; case HACKMYSTATUS.missingCommand: - msg = formatWarning("Please " +chalk.bold("give me a command") + " ("); + msg = "Please " +chalk.bold("give me a command") + " ("; msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) { return (idx === ar.length - 1 ? chalk.yellow('or ') : '') + @@ -112,79 +113,81 @@ Error-handling routines for HackMyResume. break; case HACKMYSTATUS.invalidCommand: - msg = formatWarning('Invalid command: "'+chalk.bold(ex.attempted)+'"'); + msg = 'Invalid command: "'+chalk.bold(ex.attempted)+'"'; break; case HACKMYSTATUS.resumeNotFoundAlt: - msg = formatWarning('Please ' + chalk.bold('feed me a resume') + - ' in either FRESH or JSON Resume format.'); + msg = 'Please ' + chalk.bold('feed me a resume') + + ' in either FRESH or JSON Resume format.'; break; case HACKMYSTATUS.inputOutputParity: - msg = formatWarning('Please ' + + msg = 'Please ' + chalk.bold('specify an output file name') + - ' for every input file you wish to convert.'); + ' for every input file you wish to convert.'; break; case HACKMYSTATUS.createNameMissing: - msg = formatWarning('Please ' + - chalk.bold('specify the filename of the resume') + ' to create.'); + msg = 'Please ' + + chalk.bold('specify the filename of the resume') + ' to create.'; break; case HACKMYSTATUS.pdfGeneration: - msg = formatError(chalk.bold('ERROR: PDF generation failed. ') + - 'Make sure wkhtmltopdf is installed and accessible from your path.'); + msg = chalk.bold('PDF generation failed. ') + + 'Make sure wkhtmltopdf is installed and accessible from your path.'; if( ex.inner ) msg += chalk.red('\n' + ex.inner); - withStack = true; + withStack = true; quit = false; warn = false; break; case HACKMYSTATUS.invalid: - msg = formatError('Validation failed and the --assert option was ' + - 'specified.'); + msg = 'Validation failed and the --assert option was ' + + 'specified.'; + warn = false; break; case HACKMYSTATUS.invalidFormat: ex.data.forEach(function(d){ msg += - formatWarning('The ' + chalk.bold(ex.theme.name.toUpperCase()) + + 'The ' + chalk.bold(ex.theme.name.toUpperCase()) + " theme doesn't support the " + chalk.bold(d.format.toUpperCase()) + - " format.\n"); + " format.\n"; }); break; case HACKMYSTATUS.notOnPath: - msg = formatError( ex.engine + " wasn't found on your system path or" + - " is inaccessible. PDF not generated." ); + msg = ex.engine + " wasn't found on your system path or" + + " is inaccessible. PDF not generated."; + quit = false; + warn = false; break; case HACKMYSTATUS.readError: - msg = formatError( ex.inner.toString() ); + msg = ex.inner.toString(); + warn = false; break; case HACKMYSTATUS.parseError: if( SyntaxErrorEx.is( ex.inner )) { var se = new SyntaxErrorEx( ex, ex.raw ); - msg = formatError( 'Invalid or corrupt JSON on line ' + se.line + - ' column ' + se.col ); + msg = 'Invalid or corrupt JSON on line ' + se.line + + ' column ' + se.col + '.'; } else { msg = formatError( ex.inner.toString() ); } + warn = false; break; } + return { - msg: msg, - withStack: withStack + warning: warn, // True if this is a warning, false if error + msg: msg, // The error message to display + withStack: withStack, // Whether to include the stack + quit: quit }; } - function formatError( msg ) { - return chalk.red.bold( 'ERROR: ' + msg ); - } - function formatWarning( brief, msg ) { - return chalk.yellow(brief) + chalk.yellow(msg || ''); - } }()); diff --git a/src/cli/main.js b/src/cli/main.js index d82ea0c..c06ea3e 100644 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -19,8 +19,6 @@ Definition of the `main` function. , HACKMYSTATUS = require('../core/status-codes') , HME = require('../core/event-codes') , safeLoadJSON = require('../utils/safe-json-loader') - , _opts = { } - , title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***') , StringUtils = require('../utils/string.js') , _ = require('underscore') , OUTPUT = require('./out') @@ -28,6 +26,10 @@ Definition of the `main` function. , PAD = require('string-padding') , Command = require('commander').Command; + var _opts = { }; + var _title = chalk.white.bold('\n*** HackMyResume v' +PKG.version+ ' ***'); + var _out = new OUTPUT( _opts ); + /** @@ -120,9 +122,19 @@ Definition of the `main` function. */ function initialize( ar ) { - logMsg( title ); + logMsg( _title ); var o = initOptions( ar ); + if( o.debug ) { + _out.log(chalk.cyan('The -d or --debug switch was specified. DEBUG mode engaged.')); + _out.log(''); + _out.log(chalk.cyan(PAD(' Platform:',20, null, PAD.RIGHT)) + chalk.cyan.bold( process.platform )); + _out.log(chalk.cyan(PAD(' Node.js:',20, null, PAD.RIGHT)) + chalk.cyan.bold( process.version )); + _out.log(chalk.cyan(PAD(' HackMyResume:',20, null, PAD.RIGHT)) + chalk.cyan.bold('v' + PKG.version )); + _out.log(chalk.cyan(PAD(' FRESCA:',20, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies.fresca )); + _out.log(chalk.cyan(PAD(' fresh-themes:',20, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-themes'] )); + _out.log(''); + } // Handle invalid verbs here (a bit easier here than in commander.js)... if( o.verb && !HMR.verbs[ o.verb ] && !HMR.alias[ o.verb ] ) { @@ -150,6 +162,10 @@ Definition of the `main` function. } + + /** + Init options prior to setting up command infrastructure. + */ function initOptions( ar ) { var oVerb, verb = '', args = ar.slice(), cleanArgs = args.slice(2), oJSON; if( cleanArgs.length ) { @@ -179,7 +195,13 @@ Definition of the `main` function. } } + // Grab the --debug flag + var isDebug = _.some( args, function(v) { + return v === '-d' || v === '--debug'; + }); + return { + debug: isDebug, orgVerb: oVerb, verb: verb, json: oJSON, @@ -194,10 +216,11 @@ Definition of the `main` function. function execute( src, dst, opts, log ) { loadOptions.call( this, opts, this.parent.jsonArgs ); - require( './error-handler' ).init( _opts.debug ); - var out = new OUTPUT( _opts ); + var hand = require( './error-handler' ).init( _opts.debug ); var v = new HMR.verbs[ this.name() ](); - v.on( 'hmr:status', function() { out.do.apply( out, arguments ); }); + _out.init( _opts ); + v.on( 'hmr:status', function() { _out.do.apply( _out, arguments ); }); + v.on( 'hmr:error', function() { hand.err.apply( hand, arguments ); }); v.invoke.call( v, src, dst, _opts, log ); } @@ -228,14 +251,15 @@ Definition of the `main` function. o.debug = this.parent.debug; if( o.debug ) { - logMsg(chalk.cyan('Merged options: ')); + logMsg(chalk.cyan('OPTIONS:') + '\n'); _.each(o, function(val, key) { - logMsg(chalk.cyan('%s: %s'), PAD(key,10), val); + logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'), PAD(key,17,null,PAD.RIGHT), val); }); + logMsg(''); } // Cache - _opts = o; + EXTEND(true, _opts, o); } diff --git a/src/cli/out.js b/src/cli/out.js index 704247a..2ef4198 100644 --- a/src/cli/out.js +++ b/src/cli/out.js @@ -14,50 +14,71 @@ Output routines for HackMyResume. , HME = require('../core/event-codes') , _ = require('underscore') , Class = require('../utils/class.js') + , M2C = require('../utils/md2chalk.js') , PATH = require('path') , LO = require('lodash') , FS = require('fs') + , EXTEND = require('../utils/extend') , HANDLEBARS = require('handlebars') , pad = require('string-padding'); /** - A stateful output handler. All HMR console output handled here. + A stateful output module. All HMR console output handled here. */ var OutputHandler = module.exports = Class.extend({ init: function( opts ) { - this.opts = opts; + this.opts = EXTEND( true, this.opts || { }, opts ); }, log: function( msg ) { msg = msg || ''; - this.opts.silent || console.log( msg ); + this.opts.silent || console.log.apply( console.log, arguments ); }, do: function( evt ) { + var that = this; + function L() { + that.log.apply( that, arguments ); + } + switch( evt.sub ) { + case HME.error: + //L('ERROR occured'); + break; + case HME.beforeCreate: - this.log( chalk.green('Creating new ') + - chalk.green.bold(evt.fmt) + - chalk.green(' resume: ') + chalk.green.bold(evt.file)); + L( + M2C('Creating new **%s** resume: **%s**', 'green'), + evt.fmt, evt.file + ); break; case HME.beforeRead: - this.log( chalk.cyan('Reading resume: ' + chalk.bold( evt.file ))); + L( + M2C('Reading resume: **%s**', 'cyan'), evt.file + ); + break; + + case HME.beforeTheme: + this.opts.debug && L( + M2C('Verifying theme: **%s**', 'cyan'), evt.theme.toUpperCase() + ); break; case HME.afterTheme: this.theme = evt.theme; + this.opts.debug && L( M2C('Verifying outputs: ???', 'cyan') ); break; case HME.beforeMerge: @@ -66,15 +87,14 @@ Output routines for HackMyResume. msg += ((idx === 0) ? chalk.cyan('Merging ') : chalk.cyan(' onto ')) + chalk.cyan.bold(a.i().file); }); - this.log( msg ); + L( msg ); break; case HME.afterMerge: - var numFormats = Object.keys(this.theme.formats).length; - this.log( chalk.yellow('Applying ') + - chalk.yellow.bold( this.theme.name.toUpperCase() ) + - chalk.yellow(' theme (' + numFormats + ' format' + - ( evt.numFormats === 1 ? ')' : 's)') )); + var numFormats = Object.keys( this.theme.formats ).length; + L( M2C('Applying **%s** theme (%s format%s)', 'yellow'), + this.theme.name.toUpperCase(), + numFormats, ( numFormats === 1 ? '' : 's') ); break; case HME.end: @@ -83,23 +103,21 @@ Output routines for HackMyResume. if( this.opts.tips && (this.theme.message || this.theme.render) ) { var WRAP = require('word-wrap'); if( this.theme.message ) { - this.log( WRAP( chalk.gray('The ' + themeName + ' theme says: "') + + L( WRAP( chalk.gray('The ' + themeName + ' theme says: "') + chalk.white(this.theme.message) + chalk.gray('"'), { width: this.opts.wrap, indent: '' } )); } else if ( this.theme.render ) { - this.log( WRAP( chalk.gray('The ' + themeName + - ' theme says: "') + chalk.white('For best results view JSON ' + - 'Resume themes over a local or remote HTTP connection. For ' + - 'example:'), { width: this.opts.wrap, indent: '' } - )); - this.log( ''); - this.log( - ' npm install http-server -g\r' + + L( M2C( 'The **' + themeName + '** theme says:', 'cyan')); + L( WRAP( '"For best results view JSON Resume themes over a ' + + 'local or remote HTTP connection. For example:', + { width: this.opts.wrap, indent: '' }) + ); + L(''); + L(' npm install http-server -g\r' + ' http-server ' ); - this.log(''); - this.log(chalk.white('For more information, see the README."'), - { width: this.opts.wrap, indent: '' } ); + L(''); + L(chalk.white('For more information, see the README."')); } } } @@ -113,7 +131,7 @@ Output routines for HackMyResume. suffix = chalk.green(' (with ' + this.opts.pdf + ')'); } else { - this.log( chalk.gray('Skipping ') + + L( chalk.gray('Skipping ') + chalk.white.bold( pad(evt.fmt.toUpperCase(),4,null,pad.RIGHT)) + chalk.gray(' resume') + suffix + chalk.green(': ') + chalk.white( evt.file )); @@ -122,7 +140,7 @@ Output routines for HackMyResume. } } - this.log( chalk.green('Generating ') + + L( chalk.green('Generating ') + chalk.green.bold( pad(evt.fmt.toUpperCase(),4,null,pad.RIGHT)) + chalk.green(' resume') + suffix + chalk.green(': ') + @@ -130,7 +148,7 @@ Output routines for HackMyResume. break; case HME.beforeAnalyze: - this.log(chalk.cyan('Analyzing ') + chalk.cyan.bold(evt.fmt) + + L(chalk.cyan('Analyzing ') + chalk.cyan.bold(evt.fmt) + chalk.cyan(' resume: ') + chalk.cyan.bold(evt.file)); break; @@ -148,20 +166,20 @@ Output routines for HackMyResume. case HME.beforeConvert: // TODO: Core should not log - this.log( chalk.green('Converting ') + chalk.green.bold(evt.srcFile) + + L( chalk.green('Converting ') + chalk.green.bold(evt.srcFile) + chalk.green(' (' + evt.srcFmt + ') to ') + chalk.green.bold(evt.dstFile) + chalk.green(' (' + evt.dstFmt + ').')); break; case HME.afterValidate: var style = evt.isValid ? 'green' : 'yellow'; - this.log( chalk.white('Validating ') + chalk.white.bold(evt.file) + chalk.white(' against ') + + L( chalk.white('Validating ') + chalk.white.bold(evt.file) + chalk.white(' against ') + chalk.white.bold( evt.fmt ).toUpperCase() + chalk.white(' schema: ') + chalk[style].bold(evt.isValid ? 'VALID!' : 'INVALID')); if( evt.errors ) { _.each(evt.errors, function(err,idx) { - this.log( chalk.yellow.bold('--> ') + + L( chalk.yellow.bold('--> ') + chalk.yellow(err.field.replace('data.','resume.').toUpperCase() + ' ' + err.message) ); }, this); diff --git a/src/core/resume-factory.js b/src/core/resume-factory.js index 1f5b47b..0710bb9 100644 --- a/src/core/resume-factory.js +++ b/src/core/resume-factory.js @@ -104,21 +104,25 @@ Definition of the ResumeFactory class. var rawData; try { + // Read the file eve && eve.stat( HME.beforeRead, { file: fileName }); rawData = FS.readFileSync( fileName, 'utf8' ); eve && eve.stat( HME.afterRead, { data: rawData }); + + // Parse the file eve && eve.stat( HME.beforeParse, { data: rawData }); - var ret = { - json: JSON.parse( rawData ) - }; + var ret = { json: JSON.parse( rawData ) }; eve && eve.stat( HME.afterParse, { data: ret.json } ); + return ret; } - catch( ex ) { - throw { + catch( e ) { + // Can be ENOENT, EACCES, SyntaxError, etc. + var ex = { fluenterror: rawData ? HACKMYSTATUS.parseError : HACKMYSTATUS.readError, - inner: ex, raw: rawData, file: fileName, shouldExit: false + inner: e, raw: rawData, file: fileName, shouldExit: false }; + eve && eve.err( ex.fluenterror, ex ); } } diff --git a/src/utils/md2chalk.js b/src/utils/md2chalk.js new file mode 100644 index 0000000..02b70a0 --- /dev/null +++ b/src/utils/md2chalk.js @@ -0,0 +1,18 @@ +/** +Inline Markdown-to-Chalk conversion routines. +@license MIT. See LICENSE.md for details. +@module md2chalk.js +*/ + +(function(){ + + var MD = require('marked'); + var CHALK = require('chalk'); + var LO = require('lodash'); + + module.exports = function( v, style ) { + var temp = v.replace(/\*\*(.*?)\*\*/g, CHALK.bold('$1')); + return style ? LO.get(CHALK, style)(temp) : temp; + }; + +}()); diff --git a/src/verbs/build.js b/src/verbs/build.js index 4cee4ae..c7aee50 100644 --- a/src/verbs/build.js +++ b/src/verbs/build.js @@ -66,6 +66,7 @@ Implementation of the 'build' verb for HackMyResume. // Load the theme...we do this first because the theme choice (FRESH or // JSON Resume) determines what format we'll convert the resume to. + this.stat( HME.beforeTheme, { theme: _opts.theme }); var tFolder = verifyTheme( _opts.theme ); var theme = loadTheme( tFolder ); this.stat( HME.afterTheme, { theme: theme }); @@ -73,15 +74,15 @@ Implementation of the 'build' verb for HackMyResume. // Check for invalid outputs var inv = verifyOutputs.call( this, dst, theme ); if( inv && inv.length ) { - throw {fluenterror: HACKMYSTATUS.invalidFormat, data: inv, theme: theme}; + this.err( HACKMYSTATUS.invalidFormat, { data: inv, theme: theme } ); } // Load input resumes... - if( !src || !src.length ) { throw { fluenterror: 3 }; } + if( !src || !src.length ) { this.err( HACKMYSTATUS.resumeNotFound ); } var sheets = ResumeFactory.load(src, { format: theme.render ? 'JRS' : 'FRESH', objectify: true, throw: true, inner: { sort: _opts.sort } - }, this).map(function(sh){ return sh.rez; }); + }, this).map( function(sh){ return sh.rez; }); // Merge input resumes... (sheets.length > 1) && this.stat( HME.beforeMerge, { f: _.clone(sheets) }); @@ -179,8 +180,10 @@ Implementation of the 'build' verb for HackMyResume. } } catch( ex ) { - console.log('Exception thrown'); - console.log(ex); // TODO + // Catch any errors caused by generating this file and don't let them + // propagate -- typically we want to continue processing other formats + // even if this format failed. + this.err( HME.generate, { inner: ex } ); } } @@ -282,7 +285,7 @@ Implementation of the 'build' verb for HackMyResume. if( !exists( tFolder ) ) { tFolder = PATH.resolve( themeNameOrPath ); if( !exists( tFolder ) ) { - throw { fluenterror: 1, data: _opts.theme }; + this.err( HACKMYSTATUS.themeNotFound, { data: _opts.theme } ); } } return tFolder; diff --git a/src/verbs/verb.js b/src/verbs/verb.js index 3bf4a13..2ed6cb4 100644 --- a/src/verbs/verb.js +++ b/src/verbs/verb.js @@ -23,15 +23,30 @@ Definition of the Verb class. */ var Verb = module.exports = Class.extend({ + + + /** + Constructor. Automatically called at creation. + */ init: function( moniker ) { this.moniker = moniker; this.emitter = new EVENTS.EventEmitter(); }, + + + /** + Forward subscriptions to the event emitter. + */ on: function() { this.emitter.on.apply( this.emitter, arguments ); }, + + + /** + Fire an arbitrary event, scoped to "hmr:". + */ fire: function(evtName, payload) { payload = payload || { }; payload.cmd = this.moniker; @@ -39,11 +54,29 @@ Definition of the Verb class. return true; }, + + + /** + Fire the 'hmr:error' error event. + */ + err: function( errorCode, payload, hot ) { + payload = payload || { }; + payload.sub = payload.fluenterror = errorCode; + payload.throw = hot; + this.fire('error', payload); + if( hot ) throw payload; + return true; + }, + + + + /** + Fire the 'hmr:status' error event. + */ stat: function( subEvent, payload ) { payload = payload || { }; - payload.cmd = this.moniker; payload.sub = subEvent; - this.emitter.emit( 'hmr:status', payload ); + this.fire('status', payload); return true; } diff --git a/test/test-stdout.js b/test/test-stdout.js index 7092808..68af4f1 100644 --- a/test/test-stdout.js +++ b/test/test-stdout.js @@ -70,12 +70,17 @@ describe('Testing Ouput interface', function () { run('BUILD should output a tip when no source is specified', ['build'], [ title, feedMe ]); - run('VALIATE should output a tip when no source is specified', + run('VALIDATE should output a tip when no source is specified', ['validate'], [ title, feedMe ]); run('ANALYZE should output a tip when no source is specified', ['analyze'], [ title, feedMe ]); + run('BUILD should display an error on a broken resume', + ['build', + 'node_modules/fresh-test-resumes/src/johnny-trouble.broken.fresh.json' + ], [ title, 'Error: Invalid or corrupt JSON on line' ]); + run('CONVERT should output a tip when no source is specified', ['convert'], [ title, feedMe ]);