From a54476eede679463a38f1a6c4171b5b911a59a22 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 21 Dec 2015 00:36:08 -0500 Subject: [PATCH 1/6] Reaffirm string-based generation. In recent commits, HackMyResume generation logic, much like the pilots in Top Gun who became too reliant on air-to-air missiles and lost the true art of dogfighting, has become dependent on file-based generation as implicit file assumptions have crept in. This commit reaffirms the file-less, string-based nature of the generation process and, as a side effect, adjusts the behavior of (binary) PDF generation to match. --- src/core/theme.js | 2 +- src/eng/generic-helpers.js | 2 +- src/gen/html-generator.js | 10 --- src/gen/html-pdf-generator.js | 4 +- src/gen/template-generator.js | 116 ++++++++++++++++++++++++---------- 5 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/core/theme.js b/src/core/theme.js index 68d8fb8..1eaa1ea 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -134,7 +134,7 @@ Definition of the Theme class. action: 'transform', path: absPath, major: isMajor, - orgPath: PATH.relative(that.folder, absPath), + orgPath: PATH.relative(tplFolder, absPath), ext: pathInfo.ext.slice(1), title: friendlyName( outFmt ), pre: outFmt, diff --git a/src/eng/generic-helpers.js b/src/eng/generic-helpers.js index 167c23d..ab9c1f6 100644 --- a/src/eng/generic-helpers.js +++ b/src/eng/generic-helpers.js @@ -161,7 +161,7 @@ Generic template helper definitions for FluentCV. } else { idx = Math.min( lvl / 2, 4 ); - idx = Math.max( 0, intVal ); + idx = Math.max( 0, idx ); } return idx; } diff --git a/src/gen/html-generator.js b/src/gen/html-generator.js index 41bfbcd..8b82a3d 100644 --- a/src/gen/html-generator.js +++ b/src/gen/html-generator.js @@ -22,16 +22,6 @@ Definition of the HTMLGenerator class. the HTML resume prior to saving. */ onBeforeSave: function( info ) { - var cssSrc = PATH.join( info.theme.folder, 'src', '*.css' ) - , outFolder = PATH.parse( info.outputFile ).dir, that = this; - - info.theme.cssFiles.forEach( function( f ) { - var fi = PATH.parse( f.path ); - FS.copySync( f.path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) { - throw { fluenterror: that.codes.copyCss, data: [cssSrc,cssDst] }; - }); - }); - return this.opts.prettify ? HTML.prettyPrint( info.mk, this.opts.prettify ) : info.mk; } diff --git a/src/gen/html-pdf-generator.js b/src/gen/html-pdf-generator.js index 6e32ea5..853de90 100644 --- a/src/gen/html-pdf-generator.js +++ b/src/gen/html-pdf-generator.js @@ -23,8 +23,8 @@ Definition of the HtmlPdfGenerator class. Generate the binary PDF. */ onBeforeSave: function( info ) { - pdf(info.mk, info.outputFile); - return info.mk; + pdf( info.mk, info.outputFile ); + return null; // halt further processing } }); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index fb6ee39..e12eef0 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -38,7 +38,9 @@ Definition of the TemplateGenerator class. raw: function( txt ) { return txt; }, xml: function( txt ) { return XML(txt); }, md: function( txt ) { return MD( txt || '' ); }, - mdin: function( txt ) { return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); }, + mdin: function( txt ) { + return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); + }, lower: function( txt ) { return txt.toLowerCase(); }, link: function( name, url ) { return url ? '' + name + '' : name; } @@ -69,24 +71,14 @@ Definition of the TemplateGenerator class. }, - - invoke: function( rez, themeMarkup, cssInfo, opts ) { - this.opts = EXTEND( true, {}, _defaultOpts, opts ); - mk = this.single( rez, themeMarkup, this.format, cssInfo, { } ); - this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f )); - return mk; - }, - - - /** - Default generation method for template-based generators. - @method generate + String-based template generation method. + @method invoke @param rez A FreshResume object. - @param f Full path to the output resume file to generate. @param opts Generator options. + @returns An array of strings representing generated output files. */ - generate: function( rez, f, opts ) { + invoke: function( rez, opts ) { // Carry over options this.opts = EXTEND( true, { }, _defaultOpts, opts ); @@ -96,20 +88,81 @@ Definition of the TemplateGenerator class. var theme = themeInfo.theme; var tFolder = themeInfo.folder; var tplFolder = PATH.join( tFolder, 'src' ); + var curFmt = theme.getFormat( this.format ); + var that = this; + + // "Generate": process individual files within the theme + return { + files: curFmt.files.map( function( tplInfo ) { + return { + info: tplInfo, + data: tplInfo.action === 'transform' ? + transform.call( that, rez, tplInfo, theme ) : undefined + }; + }).filter(function(item){ return item !== null; }), + themeInfo: themeInfo + }; + + }, + + + + /** + File-based template generation method. + @method generate + @param rez A FreshResume object. + @param f Full path to the output resume file to generate. + @param opts Generator options. + */ + generate: function( rez, f, opts ) { + + // Call the generation method + var genInfo = this.invoke( rez, opts ); + + // Carry over options + this.opts = EXTEND( true, { }, _defaultOpts, opts ); + + // Load the theme + var themeInfo = genInfo.themeInfo; + var theme = themeInfo.theme; + var tFolder = themeInfo.folder; + var tplFolder = PATH.join( tFolder, 'src' ); var outFolder = PATH.parse(f).dir; var curFmt = theme.getFormat( this.format ); var that = this; // "Generate": process individual files within the theme - curFmt.files.forEach(function(tplInfo){ - if( tplInfo.action === 'transform' ) { - transform.call( that, rez, f, tplInfo, theme, outFolder ); + genInfo.files.forEach(function( file ){ + + var thisFilePath; + + if( file.info.action === 'transform' ) { + thisFilePath = PATH.join( outFolder, file.info.orgPath ); + try { + if( that.onBeforeSave ) { + file.data = that.onBeforeSave({ + theme: theme, + outputFile: (file.info.major ? f : thisFilePath), + mk: file.data + }); + if( !file.data ) return; // PDF etc + } + var fileName = file.info.major ? f : thisFilePath; + MKDIRP.sync( PATH.dirname( fileName ) ); + FS.writeFileSync( fileName, file.data, + { encoding: 'utf8', flags: 'w' } ); + that.onAfterSave && that.onAfterSave( + { outputFile: fileName, mk: file.data } ); + } + catch(ex) { + console.log(ex); + } } - else if( tplInfo.action === null && theme.explicit ) { - var thisFilePath = PATH.join(outFolder, tplInfo.orgPath); + else if( file.info.action === null/* && theme.explicit*/ ) { + thisFilePath = PATH.join( outFolder, file.info.orgPath ); try { MKDIRP.sync( PATH.dirname(thisFilePath) ); - FS.copySync( tplInfo.path, thisFilePath ); + FS.copySync( file.info.path, thisFilePath ); } catch(ex) { console.log(ex); @@ -142,8 +195,10 @@ Definition of the TemplateGenerator class. */ single: function( json, jst, format, cssInfo, opts, theme ) { this.opts.freezeBreaks && ( jst = freeze(jst) ); + var eng = require( '../eng/' + theme.engine + '-generator' ); var result = eng.generate( json, jst, format, cssInfo, opts, theme ); + this.opts.freezeBreaks && ( result = unfreeze(result) ); return result; } @@ -188,18 +243,15 @@ Definition of the TemplateGenerator class. - /** - Transform a single subfile. - */ - function transform( rez, f, tplInfo, theme, outFolder ) { - var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null }; - var mk = this.single( rez, tplInfo.data, this.format, cssInfo, this.opts, theme ); - this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); - var thisFilePath = PATH.join( outFolder, tplInfo.orgPath ); + function transform( rez, tplInfo, theme ) { try { - MKDIRP.sync( PATH.dirname( tplInfo.major ? f : thisFilePath) ); - FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); - this.onAfterSave && (mk = this.onAfterSave( { outputFile: (tplInfo.major ? f : thisFilePath), mk: mk } )); + var cssInfo = { + file: tplInfo.css ? tplInfo.cssPath : null, + data: tplInfo.css || null + }; + + return this.single( rez, tplInfo.data, this.format, cssInfo, this.opts, + theme ); } catch(ex) { console.log(ex); From 65b6359fd845909d5f86f6be0eeb611a041392dc Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 21 Dec 2015 00:52:23 -0500 Subject: [PATCH 2/6] Bump version to 1.2.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae28a43..d3e0751 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hackmyresume", - "version": "1.1.0", + "version": "1.2.0", "description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.", "repository": { "type": "git", From c966f6766c7db985d2a1a8f38ac91b813a49ab9c Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 21 Dec 2015 02:56:02 -0500 Subject: [PATCH 3/6] Refactor verbs to separate files. --- src/core/default-formats.js | 19 ++ src/core/default-options.js | 13 ++ src/core/load-source-resumes.js | 13 ++ src/hackmycmd.js | 323 +------------------------------- src/index.js | 5 +- src/verbs/convert.js | 30 +++ src/verbs/create.js | 22 +++ src/verbs/generate.js | 149 +++++++++++++++ src/verbs/validate.js | 97 ++++++++++ 9 files changed, 356 insertions(+), 315 deletions(-) create mode 100644 src/core/default-formats.js create mode 100644 src/core/default-options.js create mode 100644 src/core/load-source-resumes.js create mode 100644 src/verbs/convert.js create mode 100644 src/verbs/create.js create mode 100644 src/verbs/generate.js create mode 100644 src/verbs/validate.js diff --git a/src/core/default-formats.js b/src/core/default-formats.js new file mode 100644 index 0000000..a9620bf --- /dev/null +++ b/src/core/default-formats.js @@ -0,0 +1,19 @@ +(function(){ + + var FLUENT = require('../hackmyapi'); + + /** + Supported resume formats. + */ + module.exports = [ + { name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() }, + { name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() }, + { name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() }, + { name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() }, + { name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() }, + { name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() }, + { name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() }, + { name: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() } + ]; + +}()); diff --git a/src/core/default-options.js b/src/core/default-options.js new file mode 100644 index 0000000..bc9d6ea --- /dev/null +++ b/src/core/default-options.js @@ -0,0 +1,13 @@ +(function(){ + + module.exports = { + theme: 'modern', + prettify: { // ← See https://github.com/beautify-web/js-beautify#options + indent_size: 2, + unformatted: ['em','strong'], + max_char: 80, // ← See lib/html.js in above-linked repo + //wrap_line_length: 120, ← Don't use this + } + }; + +}()); diff --git a/src/core/load-source-resumes.js b/src/core/load-source-resumes.js new file mode 100644 index 0000000..b26ae70 --- /dev/null +++ b/src/core/load-source-resumes.js @@ -0,0 +1,13 @@ +(function(){ + + var FRESHResume = require('../core/fresh-resume'); + + module.exports = function loadSourceResumes( src, log, fn ) { + return src.map( function( res ) { + log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info + + res.cyan.bold ); + return (fn && fn(res)) || (new FRESHResume()).open( res ); + }); + }; + +}()); diff --git a/src/hackmycmd.js b/src/hackmycmd.js index fe0f229..0082f8e 100644 --- a/src/hackmycmd.js +++ b/src/hackmycmd.js @@ -7,279 +7,9 @@ Internal resume generation logic for HackMyResume. (function() { module.exports = function () { - var path = require( 'path' ) - , extend = require( './utils/extend' ) - , unused = require('./utils/string') - , FS = require('fs') - , _ = require('underscore') - , FLUENT = require('./hackmyapi') - , PATH = require('path') - , MKDIRP = require('mkdirp') - //, COLORS = require('colors') - , rez, _log, _err; + var unused = require('./utils/string') + , PATH = require('path'); - /** - Given a source JSON resume, a destination resume path, and a theme file, - generate 0..N resumes in the desired formats. - @param src Path to the source JSON resume file: "rez/resume.json". - @param dst An array of paths to the target resume file(s). - @param theme Friendly name of the resume theme. Defaults to "modern". - @param logger Optional logging override. - */ - function generate( src, dst, opts, logger, errHandler ) { - - _log = logger || console.log; - _err = errHandler || error; - - //_opts = extend( true, _opts, opts ); - _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; - _opts.prettify = opts.prettify === true ? _opts.prettify : false; - - // Load input resumes... - if(!src || !src.length) { throw { fluenterror: 3 }; } - var sheets = loadSourceResumes( src ); - - // Merge input resumes... - var msg = ''; - rez = _.reduceRight( sheets, function( a, b, idx ) { - msg += ((idx == sheets.length - 2) ? - 'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName; - return extend( true, b, a ); - }); - msg && _log(msg); - - // Verify the specified theme name/path - var relativeThemeFolder = '../node_modules/fluent-themes/themes'; - var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme); - var exists = require('./utils/file-exists'); - if (!exists( tFolder )) { - tFolder = PATH.resolve( _opts.theme ); - if (!exists( tFolder )) { - throw { fluenterror: 1, data: _opts.theme }; - } - } - - // Load the theme - var theTheme = new FLUENT.Theme().open( tFolder ); - _opts.themeObj = theTheme; - _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + - (' theme (' +Object.keys(theTheme.formats).length + ' formats)').info); - - // Expand output resumes... (can't use map() here) - var targets = [], that = this; - ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { - - var to = path.resolve(t), - pa = path.parse(to), - fmat = pa.ext || '.all'; - - targets.push.apply(targets, fmat === '.all' ? - Object.keys( theTheme.formats ).map(function(k){ - var z = theTheme.formats[k]; - return { file: to.replace(/all$/g,z.outFormat), fmt: z }; - }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); - - }); - - // Run the transformation! - var finished = targets.map( function(t) { return single(t, theTheme); }); - - // Don't send the client back empty-handed - return { sheet: rez, targets: targets, processed: finished }; - } - - /** - Generate a single resume of a specific format. - @param f Full path to the destination resume to generate, for example, - "/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt". - */ - function single( targInfo, theme ) { - try { - var f = targInfo.file - , fType = targInfo.fmt.outFormat - , fName = path.basename(f, '.' + fType) - , theFormat; - - // If targInfo.fmt.files exists, this theme has an explicit "files" - // section in its theme.json file. - if( targInfo.fmt.files && targInfo.fmt.files.length ) { - - _log( 'Generating '.useful + - targInfo.fmt.outFormat.toUpperCase().useful.bold + - ' resume: '.useful + path.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold); - - theFormat = _fmts.filter( - function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; - MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; - theFormat.gen.generate( rez, f, _opts ); - - // targInfo.fmt.files.forEach( function( form ) { - // - // if( form.action === 'transform' ) { - // var theFormat = _fmts.filter( function( fmt ) { - // return fmt.name === targInfo.fmt.outFormat; - // })[0]; - // MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; - // theFormat.gen.generate( rez, f, _opts ); - // } - // else if( form.action === null ) { - // // Copy the file - // } - // - // }); - - } - // Otherwise the theme has no files section - else { - _log( 'Generating '.useful + - targInfo.fmt.outFormat.toUpperCase().useful.bold + - ' resume: '.useful + path.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold); - - theFormat = _fmts.filter( - function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; - MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; - theFormat.gen.generate( rez, f, _opts ); - } - } - catch( ex ) { - _err( ex ); - } - } - - /** - Handle an exception. - */ - function error( ex ) { - throw ex; - } - - /** - Validate 1 to N resumes in either FRESH or JSON Resume format. - */ - function validate( src, unused, opts, logger ) { - _log = logger || console.log; - if( !src || !src.length ) { throw { fluenterror: 6 }; } - var isValid = true; - - var validator = require('is-my-json-valid'); - var schemas = { - fresh: require('FRESCA'), - jars: require('./core/resume.json') - }; - - // Load input resumes... - var sheets = loadSourceResumes(src, function( res ) { - try { - return { - file: res, - raw: FS.readFileSync( res, 'utf8' ) - }; - } - catch( ex ) { - throw ex; - } - }); - - sheets.forEach( function( rep ) { - - var rez; - try { - rez = JSON.parse( rep.raw ); - } - catch( ex ) { - _log('Validating '.info + rep.file.infoBold + - ' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold); - - if (ex instanceof SyntaxError) { - // Invalid JSON - _log( '--> '.bold.red + rep.file.toUpperCase().red + - ' contains invalid JSON. Unable to validate.'.red ); - _log( (' INTERNAL: ' + ex).red ); - } - else { - - _log(('ERROR: ' + ex.toString()).red.bold); - } - return; - } - - var isValid = false; - var style = 'useful'; - var errors = []; - var fmt = rez.meta && - (rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars'; - - try { - - var validate = validator( schemas[ fmt ], { // Note [1] - formats: { - date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ - } - }); - - isValid = validate( rez ); - if( !isValid ) { - style = 'warn'; - errors = validate.errors; - } - - } - catch(ex) { - - } - - _log( 'Validating '.info + rep.file.infoBold + ' against '.info + - fmt.replace('jars','JSON Resume').toUpperCase().infoBold + - ' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold ); - - errors.forEach(function(err,idx) { - _log( '--> '.bold.yellow + - (err.field.replace('data.','resume.').toUpperCase() + ' ' + - err.message).yellow ); - }); - - }); - } - - /** - Convert between FRESH and JRS formats. - */ - function convert( src, dst, opts, logger ) { - _log = logger || console.log; - if( !src || !src.length ) { throw { fluenterror: 6 }; } - if( !dst || !dst.length ) { - if( src.length === 1 ) { throw { fluenterror: 5 }; } - else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; } - else { throw { fluenterror: 5 }; } - } - if( src && dst && src.length && dst.length && src.length !== dst.length ) { - throw { fluenterror: 7 }; - } - var sheets = loadSourceResumes( src ); - sheets.forEach(function(sheet, idx){ - var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; - var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; - _log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' + - sourceFormat + ') to ').useful + dst[0].useful.bold + - (' (' + targetFormat + ').').useful ); - sheet.saveAs( dst[idx], targetFormat ); - }); - } - - /** - Create a new empty resume in either FRESH or JRS format. - */ - function create( src, dst, opts, logger ) { - _log = logger || console.log; - dst = src || ['resume.json']; - dst.forEach( function( t ) { - var safeFormat = opts.format.toUpperCase(); - _log('Creating new '.useful +safeFormat.useful.bold + - ' resume: '.useful + t.useful.bold); - MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists; - FLUENT[ safeFormat + 'Resume' ].default().save( t ); - }); - } /** Display help documentation. @@ -289,55 +19,22 @@ Internal resume generation logic for HackMyResume. .useful.bold ); } - function loadSourceResumes( src, fn ) { - return src.map( function( res ) { - _log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info + - res.cyan.bold ); - return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res ); - }); - } - - /** - Supported resume formats. - */ - var _fmts = [ - { name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() }, - { name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() }, - { name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() }, - { name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() }, - { name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() }, - { name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() }, - { name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() }, - { name: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() } - ]; - - /** - Default HackMyResume options. - */ - var _opts = { - theme: 'modern', - prettify: { // ← See https://github.com/beautify-web/js-beautify#options - indent_size: 2, - unformatted: ['em','strong'], - max_char: 80, // ← See lib/html.js in above-linked repo - //wrap_line_length: 120, ← Don't use this - } - }; - /** Internal module interface. Used by FCV Desktop and HMR. */ return { verbs: { - build: generate, - validate: validate, - convert: convert, - new: create, + generate: require('./verbs/generate'), + build: require('./verbs/generate'), + validate: require('./verbs/validate'), + convert: require('./verbs/convert'), + create: require('./verbs/create'), + new: require('./verbs/create'), help: help }, lib: require('./hackmyapi'), - options: _opts, - formats: _fmts + options: require('./core/default-options'), + formats: require('./core/default-formats') }; }(); diff --git a/src/index.js b/src/index.js index 68d5c94..3f0bf40 100644 --- a/src/index.js +++ b/src/index.js @@ -68,7 +68,7 @@ function main() { // Massage inputs and outputs var src = a._.slice(1, splitAt === -1 ? undefined : splitAt ); var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 ); - ( splitAt === -1 ) && dst.push( src.pop() ); // Allow omitting TO keyword + ( splitAt === -1 ) && src.length > 1 && dst.push( src.pop() ); // Allow omitting TO keyword var parms = [ src, dst, opts, logMsg ]; // Invoke the action @@ -108,9 +108,10 @@ function handleError( ex ) { }).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; break; //case 4: msg = title + '\n' + ; break; - case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break; + case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created.'.guide; break; case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break; case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break; + case 8: msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold + ' to create.'.guide; break; } exitCode = ex.fluenterror; diff --git a/src/verbs/convert.js b/src/verbs/convert.js new file mode 100644 index 0000000..b6c1239 --- /dev/null +++ b/src/verbs/convert.js @@ -0,0 +1,30 @@ +(function(){ + + var loadSourceResumes = require('../core/load-source-resumes'); + + /** + Convert between FRESH and JRS formats. + */ + module.exports = function convert( src, dst, opts, logger ) { + var _log = logger || console.log; + if( !src || !src.length ) { throw { fluenterror: 6 }; } + if( !dst || !dst.length ) { + if( src.length === 1 ) { throw { fluenterror: 5 }; } + else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; } + else { throw { fluenterror: 5 }; } + } + if( src && dst && src.length && dst.length && src.length !== dst.length ) { + throw { fluenterror: 7 }; + } + var sheets = loadSourceResumes( src, _log ); + sheets.forEach(function(sheet, idx){ + var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; + var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; + _log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' + + sourceFormat + ') to ').useful + dst[0].useful.bold + + (' (' + targetFormat + ').').useful ); + sheet.saveAs( dst[idx], targetFormat ); + }); + }; + +}()); diff --git a/src/verbs/create.js b/src/verbs/create.js new file mode 100644 index 0000000..b5edf8e --- /dev/null +++ b/src/verbs/create.js @@ -0,0 +1,22 @@ +(function(){ + + var FLUENT = require('../hackmyapi') + , MKDIRP = require('mkdirp') + , PATH = require('path'); + + /** + Create a new empty resume in either FRESH or JRS format. + */ + module.exports = function create( src, dst, opts, logger ) { + var _log = logger || console.log; + if( !src || !src.length ) throw { fluenterror: 8 }; + src.forEach( function( t ) { + var safeFormat = opts.format.toUpperCase(); + _log('Creating new '.useful +safeFormat.useful.bold + + ' resume: '.useful + t.useful.bold); + MKDIRP.sync( PATH.dirname( t ) ); // Ensure dest folder exists; + FLUENT[ safeFormat + 'Resume' ].default().save( t ); + }); + }; + +}()); diff --git a/src/verbs/generate.js b/src/verbs/generate.js new file mode 100644 index 0000000..4fa10ff --- /dev/null +++ b/src/verbs/generate.js @@ -0,0 +1,149 @@ +(function() { + + var PATH = require('path') + , MKDIRP = require('mkdirp') + , _opts = require('../core/default-options') + , FluentTheme = require('../core/theme') + , loadSourceResumes = require('../core/load-source-resumes') + , _ = require('underscore') + , _fmts = require('../core/default-formats') + , _err, _log, rez; + + /** + Handle an exception. + */ + function error( ex ) { + throw ex; + } + + module.exports = + + /** + Given a source JSON resume, a destination resume path, and a theme file, + generate 0..N resumes in the desired formats. + @param src Path to the source JSON resume file: "rez/resume.json". + @param dst An array of paths to the target resume file(s). + @param theme Friendly name of the resume theme. Defaults to "modern". + @param logger Optional logging override. + */ + function generate( src, dst, opts, logger, errHandler ) { + + _log = logger || console.log; + _err = errHandler || error; + + //_opts = extend( true, _opts, opts ); + _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; + _opts.prettify = opts.prettify === true ? _opts.prettify : false; + + // Load input resumes... + if( !src || !src.length ) { throw { fluenterror: 3 }; } + var sheets = loadSourceResumes( src, _log ); + + // Merge input resumes... + var msg = ''; + rez = _.reduceRight( sheets, function( a, b, idx ) { + msg += ((idx == sheets.length - 2) ? + 'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName; + return extend( true, b, a ); + }); + msg && _log(msg); + + // Verify the specified theme name/path + var relativeThemeFolder = '../../node_modules/fluent-themes/themes'; + var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme); + var exists = require('../utils/file-exists'); + if (!exists( tFolder )) { + tFolder = PATH.resolve( _opts.theme ); + if (!exists( tFolder )) { + throw { fluenterror: 1, data: _opts.theme }; + } + } + + // Load the theme + var theTheme = new FluentTheme().open( tFolder ); + _opts.themeObj = theTheme; + _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + + (' theme (' + Object.keys(theTheme.formats).length + ' formats)').info); + + // Expand output resumes... (can't use map() here) + var targets = [], that = this; + ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { + + var to = PATH.resolve(t), + pa = PATH.parse(to), + fmat = pa.ext || '.all'; + + targets.push.apply(targets, fmat === '.all' ? + Object.keys( theTheme.formats ).map(function(k){ + var z = theTheme.formats[k]; + return { file: to.replace(/all$/g,z.outFormat), fmt: z }; + }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); + + }); + + // Run the transformation! + var finished = targets.map( function(t) { return single(t, theTheme); }); + + // Don't send the client back empty-handed + return { sheet: rez, targets: targets, processed: finished }; + }; + + /** + Generate a single resume of a specific format. + @param f Full path to the destination resume to generate, for example, + "/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt". + */ + function single( targInfo, theme ) { + try { + var f = targInfo.file + , fType = targInfo.fmt.outFormat + , fName = PATH.basename(f, '.' + fType) + , theFormat; + + // If targInfo.fmt.files exists, this theme has an explicit "files" + // section in its theme.json file. + if( targInfo.fmt.files && targInfo.fmt.files.length ) { + + _log( 'Generating '.useful + + targInfo.fmt.outFormat.toUpperCase().useful.bold + + ' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold); + + theFormat = _fmts.filter( + function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; + MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; + theFormat.gen.generate( rez, f, _opts ); + + // targInfo.fmt.files.forEach( function( form ) { + // + // if( form.action === 'transform' ) { + // var theFormat = _fmts.filter( function( fmt ) { + // return fmt.name === targInfo.fmt.outFormat; + // })[0]; + // MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; + // theFormat.gen.generate( rez, f, _opts ); + // } + // else if( form.action === null ) { + // // Copy the file + // } + // + // }); + + } + // Otherwise the theme has no files section + else { + _log( 'Generating '.useful + + targInfo.fmt.outFormat.toUpperCase().useful.bold + + ' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold); + + theFormat = _fmts.filter( + function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; + MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; + theFormat.gen.generate( rez, f, _opts ); + } + } + catch( ex ) { + _err( ex ); + } + } + +}()); diff --git a/src/verbs/validate.js b/src/verbs/validate.js new file mode 100644 index 0000000..f5fb3be --- /dev/null +++ b/src/verbs/validate.js @@ -0,0 +1,97 @@ +(function() { + + var FS = require('fs'); + var loadSourceResumes = require('../core/load-source-resumes'); + + module.exports = + + /** + Validate 1 to N resumes in either FRESH or JSON Resume format. + */ + function validate( src, unused, opts, logger ) { + var _log = logger || console.log; + if( !src || !src.length ) { throw { fluenterror: 6 }; } + var isValid = true; + + var validator = require('is-my-json-valid'); + var schemas = { + fresh: require('FRESCA'), + jars: require('../core/resume.json') + }; + + // Load input resumes... + var sheets = loadSourceResumes(src, _log, function( res ) { + try { + return { + file: res, + raw: FS.readFileSync( res, 'utf8' ) + }; + } + catch( ex ) { + throw ex; + } + }); + + sheets.forEach( function( rep ) { + + var rez; + try { + rez = JSON.parse( rep.raw ); + } + catch( ex ) { // Note [1] + _log('Validating '.info + rep.file.infoBold + + ' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold); + + if (ex instanceof SyntaxError) { + // Invalid JSON + _log( '--> '.bold.red + rep.file.toUpperCase().red + + ' contains invalid JSON. Unable to validate.'.red ); + _log( (' INTERNAL: ' + ex).red ); + } + else { + + _log(('ERROR: ' + ex.toString()).red.bold); + } + return; + } + + var isValid = false; + var style = 'useful'; + var errors = []; + var fmt = rez.meta && + (rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars'; + + try { + + var validate = validator( schemas[ fmt ], { // Note [1] + formats: { + date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ + } + }); + + isValid = validate( rez ); + if( !isValid ) { + style = 'warn'; + errors = validate.errors; + } + + } + catch(ex) { + return; + } + + _log( 'Validating '.info + rep.file.infoBold + ' against '.info + + fmt.replace('jars','JSON Resume').toUpperCase().infoBold + + ' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold ); + + errors.forEach(function(err,idx) { + _log( '--> '.bold.yellow + + (err.field.replace('data.','resume.').toUpperCase() + ' ' + + err.message).yellow ); + }); + + }); + }; + + +}()); From 1db9c2e420044e044e1abebadc613f45433ae303 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 21 Dec 2015 02:58:01 -0500 Subject: [PATCH 4/6] Fix README glitch. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6c94424..a7d2e72 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ To use HackMyResume you'll need to create a valid resume in either [FRESH][fresca] or [JSON Resume][6] format. Then you can start using the command line tool. There are four basic commands you should be aware of: -- `**build**` generates resumes in HTML, Word, Markdown, PDF, and other formats. +- **build** generates resumes in HTML, Word, Markdown, PDF, and other formats. Use it when you need to submit, upload, print, or email resumes in specific formats. @@ -58,7 +58,7 @@ formats. hackmyresume BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all ``` -- `**new**` creates a new resume in FRESH or JSON Resume format. +- **new** creates a new resume in FRESH or JSON Resume format. ```bash # hackmyresume NEW [-f ] @@ -67,7 +67,7 @@ formats. hackmyresume NEW r1.json r2.json -f jrs ``` -- `**convert**` converts your source resume between FRESH and JSON Resume +- **convert** converts your source resume between FRESH and JSON Resume formats. Use it to convert between the two formats to take advantage of tools and services. @@ -78,7 +78,7 @@ services. hackmyresume CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json ``` -- `**validate**` validates the specified resume against either the FRESH or JSON +- **validate** validates the specified resume against either the FRESH or JSON Resume schema. Use it to make sure your resume data is sufficient and complete. ```bash From 1c05846a4f8b602cdddd228e8fdbc7b44c1939a6 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 21 Dec 2015 02:58:16 -0500 Subject: [PATCH 5/6] Add CLI tests. --- tests/test-cli.js | 76 ++++++++++++++++++++++++++++++++++++++++++++ tests/test-themes.js | 4 +-- 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/test-cli.js diff --git a/tests/test-cli.js b/tests/test-cli.js new file mode 100644 index 0000000..b3d0408 --- /dev/null +++ b/tests/test-cli.js @@ -0,0 +1,76 @@ + +var chai = require('chai') + , expect = chai.expect + , should = chai.should() + , path = require('path') + , _ = require('underscore') + , FRESHResume = require('../src/core/fresh-resume') + , FCMD = require( '../src/hackmycmd') + , validator = require('is-my-json-valid') + , COLORS = require('colors'); + +chai.config.includeStack = false; + +describe('Testing CLI interface', function () { + + var _sheet; + + function logMsg() { + + } + + COLORS.setTheme({ + title: ['white','bold'], + info: process.platform === 'win32' ? 'gray' : ['white','dim'], + infoBold: ['white','dim'], + warn: 'yellow', + error: 'red', + guide: 'yellow', + status: 'gray',//['white','dim'], + useful: 'green', + }); + + var opts = { + //theme: 'compact', + format: 'FRESH', + prettify: true, + silent: false + }; + + var opts2 = { + format: 'JRS', + prettify: true, + silent: true + }; + + run( 'new', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' ); + run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' ); + run( 'new', ['tests/sandbox/new-1.json', 'tests/sandbox/new-2.json', 'tests/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' ); + run( 'new', ['tests/sandbox/new-jrs-1.json', 'tests/sandbox/new-jrs-2.json', 'tests/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' ); + run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' ); + fail( 'new', [], [], opts, " (when a filename isn't specified)" ); + + run( 'validate', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], [], opts, ' (FRESH format)' ); + run( 'validate', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' ); + + function run( verb, src, dst, opts, msg ) { + msg = msg || '.'; + it( 'The ' + verb.toUpperCase() + ' command should SUCCEED' + msg, function () { + function runIt() { + FCMD.verbs[verb]( src, dst, opts, opts.silent ? logMsg : null ); + } + runIt.should.not.Throw(); + }); + } + + function fail( verb, src, dst, opts, msg ) { + msg = msg || '.'; + it( 'The ' + verb.toUpperCase() + ' command should FAIL' + msg, function () { + function runIt() { + FCMD.verbs[verb]( src, dst, opts, logMsg ); + } + runIt.should.Throw(); + }); + } + +}); diff --git a/tests/test-themes.js b/tests/test-themes.js index a755310..9d27120 100644 --- a/tests/test-themes.js +++ b/tests/test-themes.js @@ -35,9 +35,9 @@ describe('Testing themes', function () { theme: themeName, format: 'FRESH', prettify: true, - silent: false + silent: true }; - FCMD.verbs.build( src, dst, opts ); + FCMD.verbs.build( src, dst, opts, function() { } ); } tryOpen.should.not.Throw(); }); From 31830ee759e753681ba2b5399fe6e77d12aa5074 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 21 Dec 2015 03:04:40 -0500 Subject: [PATCH 6/6] Silence tests. --- tests/test-cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-cli.js b/tests/test-cli.js index b3d0408..93be452 100644 --- a/tests/test-cli.js +++ b/tests/test-cli.js @@ -34,7 +34,7 @@ describe('Testing CLI interface', function () { //theme: 'compact', format: 'FRESH', prettify: true, - silent: false + silent: true }; var opts2 = {