diff --git a/README.md b/README.md index e58b005..4e0aa3b 100644 --- a/README.md +++ b/README.md @@ -327,5 +327,5 @@ MIT. Go crazy. See [LICENSE.md][1] for details. [travis-image]: https://img.shields.io/travis/palomajs/paloma.svg?style=flat-square [travis-url]: https://travis-ci.org/hacksalot/HackMyResume [contribute]: CONTRIBUTING.md -[fresh-themes]: https://github.com/fluentdesk/fluent-themes +[fresh-themes]: https://github.com/fluentdesk/fresh-themes [jrst]: https://www.npmjs.com/search?q=jsonresume-theme diff --git a/package.json b/package.json index 8eb6ba2..bef8d59 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "colors": "^1.1.2", "copy": "^0.1.3", - "fluent-themes": "~0.8.0-beta", + "fresh-themes": "~0.9.3-beta", "fresca": "~0.2.2", "fs-extra": "^0.24.0", "handlebars": "^4.0.5", @@ -79,6 +79,7 @@ "grunt-contrib-yuidoc": "^0.10.0", "grunt-simple-mocha": "*", "jane-q-fullstacker": "fluentdesk/jane-q-fullstacker", + "johnny-trouble-resume": "fluentdesk/johnny-trouble-resume", "jsonresume-theme-boilerplate": "^0.1.2", "jsonresume-theme-classy": "^1.0.9", "jsonresume-theme-modern": "0.0.18", diff --git a/src/core/error-handler.js b/src/core/error-handler.js index 53aa014..a2ade77 100644 --- a/src/core/error-handler.js +++ b/src/core/error-handler.js @@ -6,6 +6,9 @@ var HACKMYSTATUS = require('./status-codes') , PKG = require('../../package.json') + , FS = require('fs') + , FCMD = require('../hackmycmd') + , PATH = require('path') , title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white; var ErrorHandler = module.exports = { @@ -26,17 +29,21 @@ break; case HACKMYSTATUS.resumeNotFound: - msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + + msg = 'Please '.guide + 'feed me a resume'.guide.bold + ' in FRESH or JSON Resume format.'.guide; break; case HACKMYSTATUS.missingCommand: - msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide + - Object.keys( FCMD.verbs ).map( function(v, idx, ar) { + msg = title + "\nPlease ".guide + "give me a command".guide.bold + + " (".guide; + + msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) { return (idx === ar.length - 1 ? 'or '.guide : '') + v.toUpperCase().guide; - }).join(', '.guide) + ").\n\n".guide + - FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; + }).join(', '.guide) + ").\n\n".guide; + + msg += FS.readFileSync( + PATH.resolve(__dirname, '../use.txt'), 'utf8' ).info.bold; break; case HACKMYSTATUS.invalidCommand: @@ -45,7 +52,7 @@ break; case HACKMYSTATUS.resumeNotFoundAlt: - msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + + msg = 'Please '.guide + 'feed me a resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break; diff --git a/src/core/theme.js b/src/core/fresh-theme.js similarity index 76% rename from src/core/theme.js rename to src/core/fresh-theme.js index b9fe152..dc01975 100644 --- a/src/core/theme.js +++ b/src/core/fresh-theme.js @@ -1,11 +1,13 @@ /** -Definition of the Theme class. -@license MIT. Copyright (c) 2015 hacksalot / FluentDesk. -@module theme.js +Definition of the FRESHTheme class. +@module fresh-theme.js +@license MIT. See LICENSE.md for details. */ (function() { + + var FS = require('fs') , extend = require('../utils/extend') , validator = require('is-my-json-valid') @@ -15,20 +17,25 @@ Definition of the Theme class. , pathExists = require('path-exists').sync , EXTEND = require('../utils/extend') , moment = require('moment') - , RECURSIVE_READ_DIR = require('recursive-readdir-sync'); + , READFILES = require('recursive-readdir-sync'); + + /** - The Theme class is a representation of a HackMyResume theme asset. - @class Theme + The FRESHTheme class is a representation of a FRESH theme + asset. See also: JRSTheme. + @class FRESHTheme */ - function Theme() { + function FRESHTheme() { } + + /** Open and parse the specified theme. */ - Theme.prototype.open = function( themeFolder ) { + FRESHTheme.prototype.open = function( themeFolder ) { this.folder = themeFolder; @@ -38,18 +45,7 @@ Definition of the Theme class. // Set up a formats hash for the theme var formatsHash = { }; - // See if the theme has a package.json. If so, load it. - var packageJsonPath = PATH.join(themeFolder, 'package.json'); - if( pathExists( packageJsonPath ) ) { - var themePack = require( themeFolder ); - var themePkgJson = require( packageJsonPath ); - this.name = themePkgJson.name; - this.render = (themePack && themePack.render) || undefined; - this.formats = { html: { title: 'html', outFormat: 'html', ext: 'html', path: null, data: null } }; - return this; - } - - // Otherwise, do a full theme load + // Load the theme var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' ); var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) ); var that = this; @@ -67,10 +63,6 @@ Definition of the Theme class. formatsHash = loadImplicit.call( this ); } - // Add freebie formats every theme gets - formatsHash.json = { title: 'json', outFormat: 'json', pre: 'json', ext: 'json', path: null, data: null }; - formatsHash.yml = { title: 'yaml', outFormat: 'yml', pre: 'yml', ext: 'yml', path: null, data: null }; - // Cache this.formats = formatsHash; @@ -80,20 +72,29 @@ Definition of the Theme class. return this; }; + + /** Determine if the theme supports the specified output format. */ - Theme.prototype.hasFormat = function( fmt ) { + FRESHTheme.prototype.hasFormat = function( fmt ) { return _.has( this.formats, fmt ); }; + + /** Determine if the theme supports the specified output format. */ - Theme.prototype.getFormat = function( fmt ) { + FRESHTheme.prototype.getFormat = function( fmt ) { return this.formats[ fmt ]; }; + + /** + Load the theme implicitly, by scanning the theme folder for + files. TODO: Refactor duplicated code with loadExplicit. + */ function loadImplicit() { // Set up a hash of formats supported by this theme. @@ -107,7 +108,7 @@ Definition of the Theme class. // Iterate over all files in the theme folder, producing an array, fmts, // containing info for each file. While we're doing that, also build up // the formatsHash object. - var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) { + var fmts = READFILES(tplFolder).map( function(absPath) { // If this file lives in a specific format folder within the theme, // such as "/latex" or "/html", then that format is the output format @@ -135,7 +136,7 @@ Definition of the Theme class. // compact-[outputformat].[extension], for ex, compact-pdf.html. if( !outFmt ) { var idx = pathInfo.name.lastIndexOf('-'); - outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 ); + outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1); isMajor = true; } @@ -165,9 +166,13 @@ Definition of the Theme class. }); // Now, get all the CSS files... - (this.cssFiles = fmts.filter(function( fmt ){ return fmt && (fmt.ext === 'css'); })) + (this.cssFiles = fmts.filter(function( fmt ){ + return fmt && (fmt.ext === 'css'); + })) + + // For each CSS file, get its corresponding HTML file .forEach(function( cssf ) { - // For each CSS file, get its corresponding HTML file + var idx = _.findIndex(fmts, function( fmt ) { return fmt && fmt.pre === cssf.pre && fmt.ext === 'html'; }); @@ -184,33 +189,39 @@ Definition of the Theme class. return formatsHash; } + + + /** + Load the theme explicitly, by following the 'formats' hash + in the theme's JSON settings file. + */ function loadExplicit() { - var that = this; - // Set up a hash of formats supported by this theme. + // Housekeeping var formatsHash = { }; - - // Establish the base theme folder var tplFolder = PATH.join( this.folder, 'src' ); - var act = null; + var that = this; // Iterate over all files in the theme folder, producing an array, fmts, // containing info for each file. While we're doing that, also build up // the formatsHash object. - var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) { + var fmts = READFILES( tplFolder ).map( function( absPath ) { act = null; // If this file is mentioned in the theme's JSON file under "transforms" var pathInfo = parsePath(absPath); var absPathSafe = absPath.trim().toLowerCase(); - var outFmt = _.find( Object.keys( that.formats ), function( fmtKey ) { - var fmtVal = that.formats[ fmtKey ]; - return _.some( fmtVal.transform, function( fpath ) { - var absPathB = PATH.join( that.folder, fpath ).trim().toLowerCase(); - return absPathB === absPathSafe; + var outFmt = _.find( + Object.keys( that.formats ), + function( fmtKey ) { + var fmtVal = that.formats[ fmtKey ]; + return _.some( fmtVal.transform, function(fpath) { + var absPathB = PATH.join( that.folder, fpath ) + .trim().toLowerCase(); + return absPathB === absPathSafe; + }); }); - }); if( outFmt ) { act = 'transform'; } @@ -231,7 +242,7 @@ Definition of the Theme class. // compact-[outputformat].[extension], for ex, compact-pdf.html. if( !outFmt ) { var idx = pathInfo.name.lastIndexOf('-'); - outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 ); + outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1); } // We should have a valid output format now. @@ -261,7 +272,11 @@ Definition of the Theme class. }); // Now, get all the CSS files... - (this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; })) + (this.cssFiles = fmts.filter(function( fmt ){ + return fmt.ext === 'css'; + })) + + // For each CSS file, get its corresponding HTML file .forEach(function( cssf ) { // For each CSS file, get its corresponding HTML file var idx = _.findIndex(fmts, function( fmt ) { @@ -279,12 +294,22 @@ Definition of the Theme class. return formatsHash; } + + + /** + Return a more friendly name for certain formats. + TODO: Refactor + */ function friendlyName( val ) { val = val.trim().toLowerCase(); var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }; return friendly[val] || val; } - module.exports = Theme; + + + module.exports = FRESHTheme; + + }()); diff --git a/src/core/jrs-theme.js b/src/core/jrs-theme.js new file mode 100644 index 0000000..4f2942a --- /dev/null +++ b/src/core/jrs-theme.js @@ -0,0 +1,85 @@ +/** +Definition of the JRSTheme class. +@module jrs-theme.js +@license MIT. See LICENSE.MD for details. +*/ + +(function() { + + + + var _ = require('underscore') + , PATH = require('path') + , parsePath = require('parse-filepath') + , pathExists = require('path-exists').sync; + + + + /** + The JRSTheme class is a representation of a JSON Resume + theme asset. See also: FRESHTheme. + @class JRSTheme + */ + function JRSTheme() { + + } + + + + /** + Open and parse the specified theme. + @method open + */ + JRSTheme.prototype.open = function( thFolder ) { + + this.folder = thFolder; + + // Open the [theme-name].json file; should have the same + // name as folder + var pathInfo = parsePath( thFolder ); + + // Open and parse the theme's package.json file. + var pkgJsonPath = PATH.join( thFolder, 'package.json' ); + if( pathExists( pkgJsonPath )) { + var thApi = require( thFolder ) + , thPkg = require( pkgJsonPath ); + this.name = thPkg.name; + this.render = (thApi && thApi.render) || undefined; + this.formats = { + html: { title:'html', outFormat:'html', ext:'html' } + }; + } + else { + throw { fluenterror: 10 }; + } + + return this; + }; + + + + /** + Determine if the theme supports the output format. + @method hasFormat + */ + JRSTheme.prototype.hasFormat = function( fmt ) { + return _.has( this.formats, fmt ); + }; + + + + /** + Return the requested output format. + @method getFormat + */ + JRSTheme.prototype.getFormat = function( fmt ) { + return this.formats[ fmt ]; + }; + + + + module.exports = JRSTheme; + + + +}()); diff --git a/src/core/status-codes.js b/src/core/status-codes.js index d0467c7..400467f 100644 --- a/src/core/status-codes.js +++ b/src/core/status-codes.js @@ -1,6 +1,7 @@ /** Status codes for HackMyResume. @module status-codes.js +@license MIT. See LICENSE.MD for details. */ (function(){ @@ -15,7 +16,8 @@ Status codes for HackMyResume. resumeNotFoundAlt: 6, inputOutputParity: 7, createNameMissing: 8, - wkhtmltopdf: 9 + wkhtmltopdf: 9, + missingPackageJSON: 10 }; }()); diff --git a/src/eng/generic-helpers.js b/src/eng/generic-helpers.js index ab9c1f6..a930933 100644 --- a/src/eng/generic-helpers.js +++ b/src/eng/generic-helpers.js @@ -1,6 +1,6 @@ /** -Generic template helper definitions for FluentCV. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +Generic template helper definitions for HackMyResume / FluentCV. +@license MIT. See LICENSE.md for details. @module generic-helpers.js */ @@ -114,6 +114,16 @@ Generic template helper definitions for FluentCV. if (lhs || rhs) return options.fn(this); }, + /** + Conditional stylesheet link. Either display the link or embed the stylesheet + via tag. + */ + styleSheet: function( file, options ) { + return ( this.opts.css === 'link') ? + '' : + ''; + }, + /** Perform a generic comparison. See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates diff --git a/src/eng/handlebars-generator.js b/src/eng/handlebars-generator.js index 71dbf58..27a8f7d 100644 --- a/src/eng/handlebars-generator.js +++ b/src/eng/handlebars-generator.js @@ -40,6 +40,7 @@ Definition of the HandlebarsGenerator class. RAW: json, filt: opts.filters, cssInfo: cssInfo, + opts: opts, headFragment: opts.headFragment || '' }); diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js index 0a9be84..eaa8eed 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -11,7 +11,6 @@ Definition of the UnderscoreGenerator class. var _ = require('underscore'); - /** Perform template-based resume generation using Underscore.js. @class UnderscoreGenerator @@ -32,6 +31,10 @@ Definition of the UnderscoreGenerator class. // Strip {# comments #} jst = jst.replace( delims.comment, ''); + var helpers = require('./generic-helpers'); + helpers.opts = opts; + helpers.cssInfo = cssInfo; + // Compile and run the template. TODO: avoid unnecessary recompiles. var compiled = _.template(jst); var ret = compiled({ @@ -40,13 +43,15 @@ Definition of the UnderscoreGenerator class. XML: require('xml-escape'), RAW: json, cssInfo: cssInfo, - headFragment: opts.headFragment || '' + headFragment: opts.headFragment || '', + opts: opts, + h: helpers }); return ret; } }; - + }()); diff --git a/src/gen/base-generator.js b/src/gen/base-generator.js index 5d11777..b8a9538 100644 --- a/src/gen/base-generator.js +++ b/src/gen/base-generator.js @@ -1,7 +1,7 @@ /** Definition of the BaseGenerator class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module base-generator.js +@license MIT. See LICENSE.md for details. */ (function() { diff --git a/src/gen/html-png-generator.js b/src/gen/html-png-generator.js index 4d9b5b9..2ec793e 100644 --- a/src/gen/html-png-generator.js +++ b/src/gen/html-png-generator.js @@ -1,6 +1,6 @@ /** Definition of the HtmlPngGenerator class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@license MIT. See LICENSE.MD for details. @module html-png-generator.js */ @@ -11,7 +11,7 @@ Definition of the HtmlPngGenerator class. , HTML = require( 'html' ); /** - An HTML-based PDF resume generator for HackMyResume. + An HTML-based PNG resume generator for HackMyResume. */ var HtmlPngGenerator = module.exports = TemplateGenerator.extend({ @@ -19,24 +19,29 @@ Definition of the HtmlPngGenerator class. this._super( 'png', 'html' ); }, - /** - Generate the binary PDF. - */ - onBeforeSave: function( info ) { - png( info.mk, info.outputFile ); - return null; // halt further processing + invoke: function( rez, themeMarkup, cssInfo, opts ) { + //return YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 ); + }, + + generate: function( rez, f, opts ) { + var htmlResults = opts.targets.filter(function(t){ + return t.fmt.outFormat === 'html'; + }); + var htmlFile = htmlResults[0].final.files.filter(function(fl){ + return fl.info.ext === 'html'; + }); + png(htmlFile[0].data, f); } }); /** - Generate a PDF from HTML. + Generate a PNG from HTML. */ function png( markup, fOut ) { - - require('webshot')( markup , { encoding: 'binary', siteType: 'html' } ) - .pipe( FS.createWriteStream( fOut ) ); - + // require('webshot')( markup , { encoding: 'binary', siteType: 'html' } ) + // .pipe( FS.createWriteStream( fOut ) ); + require('webshot')( markup , fOut, { siteType: 'html' }, function(err) { } ); } }()); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 60a7a6f..257f477 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -1,6 +1,6 @@ /** Definition of the TemplateGenerator class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@license MIT. See LICENSE.md for details. @module template-generator.js */ @@ -17,7 +17,8 @@ Definition of the TemplateGenerator class. , MKDIRP = require('mkdirp') , BaseGenerator = require( './base-generator' ) , EXTEND = require('../utils/extend') - , Theme = require('../core/theme'); + , FRESHTheme = require('../core/fresh-theme') + , JRSTheme = require('../core/jrs-theme'); @@ -77,7 +78,14 @@ Definition of the TemplateGenerator class. @method invoke @param rez A FreshResume object. @param opts Generator options. - @returns An array of strings representing generated output files. + @returns An array of objects representing the generated output files. Each + object has this format: + + { + files: [ { info: { }, data: [ ] }, { ... } ], + themeInfo: { } + } + */ invoke: function( rez, opts ) { @@ -182,6 +190,8 @@ Definition of the TemplateGenerator class. }); } + return genInfo; + }, @@ -220,20 +230,28 @@ Definition of the TemplateGenerator class. Given a theme title, load the corresponding theme. */ function themeFromMoniker() { + // Verify the specified theme name/path var tFolder = PATH.join( - parsePath( require.resolve('fluent-themes') ).dirname, + parsePath( require.resolve('fresh-themes') ).dirname, this.opts.theme ); - var exists = require('path-exists').sync; - if( !exists( tFolder ) ) { - tFolder = PATH.resolve( this.opts.theme ); - if( !exists( tFolder ) ) { - throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme}; - } - } - var t = this.opts.themeObj || new Theme().open( tFolder ); + var t; + if( this.opts.theme.startsWith('jsonresume-theme-') ) { + console.log('LOADING JSON RESUME'); + t = new JRSTheme().open( tFolder ); + } + else { + var exists = require('path-exists').sync; + if( !exists( tFolder ) ) { + tFolder = PATH.resolve( this.opts.theme ); + if( !exists( tFolder ) ) { + throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme}; + } + } + t = this.opts.themeObj || new FRESHTheme().open( tFolder ); + } // Load the theme and format return { diff --git a/src/hackmyapi.js b/src/hackmyapi.js index f36c40a..a118028 100644 --- a/src/hackmyapi.js +++ b/src/hackmyapi.js @@ -8,7 +8,8 @@ module.exports = { Sheet: require('./core/fresh-resume'), FRESHResume: require('./core/fresh-resume'), JRSResume: require('./core/jrs-resume'), - Theme: require('./core/theme'), + FRESHTheme: require('./core/fresh-theme'), + JRSTheme: require('./core/jrs-theme'), FluentDate: require('./core/fluent-date'), HtmlGenerator: require('./gen/html-generator'), TextGenerator: require('./gen/text-generator'), diff --git a/src/hackmycmd.js b/src/hackmycmd.js index 0082f8e..ad62e4e 100644 --- a/src/hackmycmd.js +++ b/src/hackmycmd.js @@ -8,29 +8,34 @@ Internal resume generation logic for HackMyResume. module.exports = function () { var unused = require('./utils/string') - , PATH = require('path'); + , PATH = require('path') + , FS = require('fs'); /** Display help documentation. */ function help() { - console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ) - .useful.bold ); + var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ); + console.log( manPage.useful.bold ); } /** Internal module interface. Used by FCV Desktop and HMR. */ + var v = { + build: require('./verbs/generate'), + validate: require('./verbs/validate'), + convert: require('./verbs/convert'), + new: require('./verbs/create'), + help: help + }; + return { - verbs: { - 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 + verbs: v, + alias: { + generate: v.build, + create: v.build }, lib: require('./hackmyapi'), options: require('./core/default-options'), diff --git a/src/index.js b/src/index.js index 48d2503..9352d49 100644 --- a/src/index.js +++ b/src/index.js @@ -55,7 +55,7 @@ function main() { // Get the action to be performed var params = a._.map( function(p){ return p.toLowerCase().trim(); }); var verb = params[0]; - if( !FCMD.verbs[ verb ] ) { + if( !FCMD.verbs[ verb ] && !FCMD.alias[ verb ] ) { logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn); return; } @@ -74,10 +74,9 @@ function main() { var src = a._.slice(1, splitAt === -1 ? undefined : splitAt ); var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 ); ( splitAt === -1 ) && src.length > 1 && dst.push( src.pop() ); // Allow omitting TO keyword - var parms = [ src, dst, opts, logMsg ]; // Invoke the action - FCMD.verbs[ verb ].apply( null, parms ); + (FCMD.verbs[verb] || FCMD.alias[verb]).apply(null, [src, dst, opts, logMsg]); } @@ -92,6 +91,7 @@ function getOpts( args ) { theme: args.t || 'modern', format: args.f || 'FRESH', prettify: !noPretty, - silent: args.s || args.silent + silent: args.s || args.silent, + css: args.css || 'embed' }; } diff --git a/src/verbs/generate.js b/src/verbs/generate.js index 2291f53..55011e3 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -1,17 +1,29 @@ +/** +Implementation of the 'generate' verb for HackMyResume. +@module generate.js +@license MIT. See LICENSE.md for details. +*/ + (function() { + + var PATH = require('path') , FS = require('fs') - , parsePath = require('parse-filepath') , MD = require('marked') , MKDIRP = require('mkdirp') + , EXTEND = require('../utils/extend') + , parsePath = require('parse-filepath') , _opts = require('../core/default-options') - , FluentTheme = require('../core/theme') + , FluentTheme = require('../core/fresh-theme') + , JRSTheme = require('../core/jrs-theme') , ResumeFactory = require('../core/resume-factory') , _ = require('underscore') , _fmts = require('../core/default-formats') , _err, _log, rez; + + /** Handle an exception. */ @@ -19,42 +31,29 @@ 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. + Given a source resume in FRESH or JRS format, 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 ) { + function build( src, dst, opts, logger, errHandler ) { + // Housekeeping... _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; + _opts.css = opts.css; - // Verify the specified theme name/path - var relativeThemeFolder = '../../node_modules/fluent-themes/themes'; - var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme); - var exists = require('path-exists').sync; - 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; - var numFormats = theTheme.formats ? Object.keys(theTheme.formats).length : 2; - _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + - (' theme (' + numFormats + ' formats)').info); + // Load the theme... + var tFolder = verify_theme( _opts.theme ); + var theTheme = load_theme( tFolder ); // Load input resumes... if( !src || !src.length ) { throw { fluenterror: 3 }; } @@ -70,41 +69,29 @@ }); msg && _log(msg); - // 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 = parsePath(to), - fmat = pa.extname || '.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) ) }]); - - }); + // Expand output resumes... + var targets = expand( dst, theTheme ); // Run the transformation! - var finished = targets.map( function(t) { return single(t, theTheme); }); + targets.forEach( function(t) { + t.final = single( t, theTheme, targets ); + }); // Don't send the client back empty-handed - return { sheet: rez, targets: targets, processed: finished }; - }; + return { sheet: rez, targets: targets, processed: targets }; + } + + /** - 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". + Generate a single target resume such as "out/rez.html" or "out/rez.doc". + @param targInfo Information for the target resume. + @param theme A FRESHTheme or JRSTheme object. + @returns */ - function single( targInfo, theme ) { + function single( targInfo, theme, finished ) { - function MDIN(txt) { + function MDIN(txt) { // TODO: Move this return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); } @@ -114,32 +101,32 @@ , 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); + ' resume: '.useful + PATH.relative(process.cwd(), f ).useful.bold ); + // If targInfo.fmt.files exists, this format is backed by a document. + // Fluent/FRESH themes are handled here. + if( targInfo.fmt.files && targInfo.fmt.files.length ) { 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 ); + _opts.targets = finished; + return theFormat.gen.generate( rez, f, _opts ); } - // 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]; + // Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format + // (JSON, YML, or PNG) that every theme gets "for free". + else { + + theFormat = _fmts.filter( function(fmt) { + return fmt.name === targInfo.fmt.outFormat; + })[0]; var outFolder = PATH.dirname( f ); MKDIRP.sync( outFolder ); // Ensure dest folder exists; - // TODO: refactor + // JSON Resume themes have a 'render' method that needs to be called if( theme.render ) { var COPY = require('copy'); var globs = [ /*'**',*/ '*.css', '*.js', '*.png', '*.jpg', '*.gif', '*.bmp' ]; @@ -151,7 +138,7 @@ // } }); - // Prevent JSON Resume theme .js from chattering + // Prevent JSON Resume theme .js from chattering (TODO: redirect IO) var consoleLog = console.log; console.log = function() { }; @@ -169,9 +156,12 @@ // Save the file FS.writeFileSync( f, rezHtml ); + + // Return markup to the client + return rezHtml; } else { - theFormat.gen.generate( rez, f, _opts ); + return theFormat.gen.generate( rez, f, _opts ); } } } @@ -180,4 +170,117 @@ } } + + + /** + Expand output files. For example, "foo.all" should be expanded to + ["foo.html", "foo.doc", "foo.pdf", "etc"]. + @param dst An array of output files as specified by the user. + @param theTheme A FRESHTheme or JRSTheme object. + */ + function expand( dst, theTheme ) { + + // Add freebie formats (JSON, YAML, PNG) every theme gets... + // Add HTML-driven PNG only if the theme has an HTML format. + theTheme.formats.json = theTheme.formats.json || { + freebie: true, title: 'json', outFormat: 'json', pre: 'json', + ext: 'json', path: null, data: null + }; + theTheme.formats.yml = theTheme.formats.yml || { + freebie: true, title: 'yaml', outFormat: 'yml', pre: 'yml', + ext: 'yml', path: null, data: null + }; + if( theTheme.formats.html && !theTheme.formats.png ) { + theTheme.formats.png = { + freebie: true, title: 'png', outFormat: 'png', + ext: 'yml', path: null, data: null + }; + } + + // Set up the destination collection. It's either the array of files passed + // by the user or 'out/resume.all' if no targets were specified. + var destColl = (dst && dst.length && dst) || + [PATH.normalize('out/resume.all')]; + + // Assemble an array of expanded target files... (can't use map() here) + var targets = []; + destColl.forEach( function(t) { + + var to = PATH.resolve(t), pa = parsePath(to),fmat = pa.extname || '.all'; + + var explicitFormats = _.omit( theTheme.formats, function(val, key) { + return !val.freebie; + }); + var implicitFormats = _.omit( theTheme.formats, function(val) { + return val.freebie; + }); + + targets.push.apply( + targets, fmat === '.all' ? + Object.keys( implicitFormats ).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) ) }]); + + targets.push.apply( + targets, fmat === '.all' ? + Object.keys( explicitFormats ).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) ) }]); + + }); + + return targets; + } + + + /** + Verify the specified theme name/path. + */ + function verify_theme( themeNameOrPath ) { + var tFolder = PATH.resolve( + __dirname, + '../../node_modules/fresh-themes/themes', + themeNameOrPath + ); + var exists = require('path-exists').sync; + if( !exists( tFolder ) ) { + tFolder = PATH.resolve( themeNameOrPath ); + if( !exists( tFolder ) ) { + throw { fluenterror: 1, data: _opts.theme }; + } + } + return tFolder; + } + + + + /** + Load the specified theme. + */ + function load_theme( tFolder ) { + + // Create a FRESH or JRS theme object + var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ? + new JRSTheme().open(tFolder) : new FluentTheme().open( tFolder ); + + // Cache the theme object + _opts.themeObj = theTheme; + + // Output a message TODO: core should not log + var numFormats = Object.keys(theTheme.formats).length; + _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + + (' theme (' + numFormats + ' formats)').info); + return theTheme; + } + + + + module.exports = build; + + + }()); diff --git a/test/test-fresh-sheet.js b/test/test-fresh-sheet.js index 4d28559..eb2ec7c 100644 --- a/test/test-fresh-sheet.js +++ b/test/test-fresh-sheet.js @@ -9,59 +9,54 @@ var chai = require('chai') chai.config.includeStack = false; -describe('jane-doe.json (FRESH)', function () { +function testResume(opts) { - var _sheet; + describe( opts.title + ' (FRESH)', function () { - it('should open without throwing an exception', function () { - function tryOpen() { - _sheet = new FRESHResume().open( - 'node_modules/jane-q-fullstacker/resume/jane-resume.json' ); - } - tryOpen.should.not.Throw(); - }); + var _sheet; - it('should have one or more of each section', function() { - expect( - //(_sheet.basics) && - _sheet.name && _sheet.info && _sheet.location && _sheet.contact && - (_sheet.employment.history && _sheet.employment.history.length > 0) && - (_sheet.skills && _sheet.skills.list.length > 0) && - (_sheet.education.history && _sheet.education.history.length > 0) && - (_sheet.service.history && _sheet.service.history.length > 0) && - (_sheet.writing && _sheet.writing.length > 0) && - (_sheet.recognition && _sheet.recognition.length > 0) && - (_sheet.samples && _sheet.samples.length > 0) && - (_sheet.references && _sheet.references.length > 0) && - (_sheet.interests && _sheet.interests.length > 0) - ).to.equal( true ); - }); + it('should open without throwing an exception', function () { + function tryOpen() { + _sheet = new FRESHResume().open( opts.path ); + } + tryOpen.should.not.Throw(); + }); - it('should have a work duration of 7 years', function() { - _sheet.computed.numYears.should.equal( 7 ); - }); + it('should have one or more of each section', function() { + var newObj = _.pick( _sheet, opts.sections ); + expect( Object.keys(newObj).length ).to.equal( opts.sections.length ); + }); - it('should save without throwing an exception', function(){ - function trySave() { - _sheet.save( 'test/sandbox/jane-q-fullstacker.json' ); - } - trySave.should.not.Throw(); - }); + it('should have a work duration of ' + opts.duration + ' years', function() { + _sheet.computed.numYears.should.equal( opts.duration ); + }); - it('should not be modified after saving', function() { - var savedSheet = new FRESHResume().open('test/sandbox/jane-q-fullstacker.json'); - _sheet.stringify().should.equal( savedSheet.stringify() ); - }); + it('should save without throwing an exception', function(){ + function trySave() { + _sheet.save( 'test/sandbox/' + opts.title + '.json' ); + } + trySave.should.not.Throw(); + }); - it('should validate against the FRESH resume schema', function() { - var result = _sheet.isValid(); - // var schemaJson = require('fresca'); - // var validate = validator( schemaJson, { verbose: true } ); - // var result = validate( JSON.parse( _sheet.imp.raw ) ); - result || console.log("\n\nOops, resume didn't validate. " + - "Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n"); - result.should.equal( true ); - }); + it('should not be modified after saving', function() { + var savedSheet = new FRESHResume().open('test/sandbox/' + opts.title + '.json'); + _sheet.stringify().should.equal( savedSheet.stringify() ); + }); + + it('should validate against the FRESH resume schema', function() { + var result = _sheet.isValid(); + // var schemaJson = require('fresca'); + // var validate = validator( schemaJson, { verbose: true } ); + // var result = validate( JSON.parse( _sheet.imp.raw ) ); + result || console.log("\n\nOops, resume didn't validate. " + + "Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n"); + result.should.equal( true ); + }); -}); + }); +} + +var sects = [ 'info', 'employment', 'service', 'skills', 'education', 'writing', 'recognition', 'references' ]; +testResume({ title: 'jane-q-fullstacker', path: 'node_modules/jane-q-fullstacker/resume/jane-resume.json', duration: 7, sections: sects }); +testResume({ title: 'johnny-trouble-resume', path: 'node_modules/johnny-trouble-resume/src/johnny-trouble.fresh.json', duration: 3, sections: sects }); diff --git a/test/test-themes.js b/test/test-themes.js index 5a6ee1d..ba9496a 100644 --- a/test/test-themes.js +++ b/test/test-themes.js @@ -12,7 +12,9 @@ var SPAWNWATCHER = require('../src/core/spawn-watch') chai.config.includeStack = false; -describe('Testing themes', function () { +function genThemes( title, src, fmt ) { + + describe('Testing themes against ' + title.toUpperCase() + ' resume ' + '(' + fmt + ')' , function () { var _sheet; @@ -29,11 +31,11 @@ describe('Testing themes', function () { function genTheme( fmt, src, themeName, themeLoc, testTitle ) { themeLoc = themeLoc || themeName; - testTitle = themeName.toUpperCase() + ' theme should generate without throwing an exception'; + testTitle = themeName.toUpperCase() + ' theme (' + fmt + ') should generate without throwing an exception'; it( testTitle, function () { function tryOpen() { //var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; - var dst = ['test/sandbox/' + fmt + '/' + themeName + '/resume.all']; + var dst = ['test/sandbox/' + fmt + '/' + title + '/' + themeName + '/resume.all']; var opts = { theme: themeLoc, format: fmt, @@ -46,28 +48,21 @@ describe('Testing themes', function () { }); } - var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; - genTheme('FRESH', src, 'hello-world'); - genTheme('FRESH', src, 'compact'); - genTheme('FRESH', src, 'modern'); - genTheme('FRESH', src, 'minimist'); - genTheme('FRESH', src, 'awesome'); - genTheme('FRESH', src, 'positive'); - genTheme('FRESH', src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' ); - genTheme('FRESH', src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' ); - genTheme('FRESH', src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' ); - genTheme('FRESH', src, 'jsonresume-theme-classy', 'node_modules/jsonresume-theme-classy' ); + genTheme(fmt, src, 'hello-world'); + genTheme(fmt, src, 'compact'); + genTheme(fmt, src, 'modern'); + genTheme(fmt, src, 'minimist'); + genTheme(fmt, src, 'awesome'); + genTheme(fmt, src, 'positive'); + genTheme(fmt, src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' ); + genTheme(fmt, src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' ); + genTheme(fmt, src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' ); + genTheme(fmt, src, 'jsonresume-theme-classy', 'node_modules/jsonresume-theme-classy' ); - src = ['test/resumes/jrs-0.0.0/richard-hendriks.json']; - genTheme('JRS', src, 'hello-world'); - genTheme('JRS', src, 'compact'); - genTheme('JRS', src, 'modern'); - genTheme('JRS', src, 'minimist'); - genTheme('JRS', src, 'awesome'); - genTheme('JRS', src, 'positive'); - genTheme('JRS', src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' ); - genTheme('JRS', src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' ); - genTheme('JRS', src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' ); - genTheme('JRS', src, 'jsonresume-theme-classy', 'node_modules/jsonresume-theme-classy' ); + }); -}); +} + +genThemes( 'jane-q-fullstacker', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], 'FRESH' ); +genThemes( 'johnny-trouble', ['node_modules/johnny-trouble-resume/src/johnny-trouble.fresh.json'], 'FRESH' ); +genThemes( 'richard-hendriks', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], 'JRS' );