1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-07-07 02:00:06 +01:00
This commit is contained in:
hacksalot 2015-12-30 22:03:38 -05:00
commit 2b3c83c57e
18 changed files with 499 additions and 241 deletions

View File

@ -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-image]: https://img.shields.io/travis/palomajs/paloma.svg?style=flat-square
[travis-url]: https://travis-ci.org/hacksalot/HackMyResume [travis-url]: https://travis-ci.org/hacksalot/HackMyResume
[contribute]: CONTRIBUTING.md [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 [jrst]: https://www.npmjs.com/search?q=jsonresume-theme

View File

@ -47,7 +47,7 @@
"dependencies": { "dependencies": {
"colors": "^1.1.2", "colors": "^1.1.2",
"copy": "^0.1.3", "copy": "^0.1.3",
"fluent-themes": "~0.8.0-beta", "fresh-themes": "~0.9.3-beta",
"fresca": "~0.2.2", "fresca": "~0.2.2",
"fs-extra": "^0.24.0", "fs-extra": "^0.24.0",
"handlebars": "^4.0.5", "handlebars": "^4.0.5",
@ -79,6 +79,7 @@
"grunt-contrib-yuidoc": "^0.10.0", "grunt-contrib-yuidoc": "^0.10.0",
"grunt-simple-mocha": "*", "grunt-simple-mocha": "*",
"jane-q-fullstacker": "fluentdesk/jane-q-fullstacker", "jane-q-fullstacker": "fluentdesk/jane-q-fullstacker",
"johnny-trouble-resume": "fluentdesk/johnny-trouble-resume",
"jsonresume-theme-boilerplate": "^0.1.2", "jsonresume-theme-boilerplate": "^0.1.2",
"jsonresume-theme-classy": "^1.0.9", "jsonresume-theme-classy": "^1.0.9",
"jsonresume-theme-modern": "0.0.18", "jsonresume-theme-modern": "0.0.18",

View File

@ -6,6 +6,9 @@
var HACKMYSTATUS = require('./status-codes') var HACKMYSTATUS = require('./status-codes')
, PKG = require('../../package.json') , PKG = require('../../package.json')
, FS = require('fs')
, FCMD = require('../hackmycmd')
, PATH = require('path')
, title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white; , title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white;
var ErrorHandler = module.exports = { var ErrorHandler = module.exports = {
@ -26,17 +29,21 @@
break; break;
case HACKMYSTATUS.resumeNotFound: 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; ' in FRESH or JSON Resume format.'.guide;
break; break;
case HACKMYSTATUS.missingCommand: case HACKMYSTATUS.missingCommand:
msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide + msg = title + "\nPlease ".guide + "give me a command".guide.bold +
Object.keys( FCMD.verbs ).map( function(v, idx, ar) { " (".guide;
msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? 'or '.guide : '') + return (idx === ar.length - 1 ? 'or '.guide : '') +
v.toUpperCase().guide; v.toUpperCase().guide;
}).join(', '.guide) + ").\n\n".guide + }).join(', '.guide) + ").\n\n".guide;
FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold;
msg += FS.readFileSync(
PATH.resolve(__dirname, '../use.txt'), 'utf8' ).info.bold;
break; break;
case HACKMYSTATUS.invalidCommand: case HACKMYSTATUS.invalidCommand:
@ -45,7 +52,7 @@
break; break;
case HACKMYSTATUS.resumeNotFoundAlt: 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; ' in either FRESH or JSON Resume format.'.guide;
break; break;

View File

@ -1,11 +1,13 @@
/** /**
Definition of the Theme class. Definition of the FRESHTheme class.
@license MIT. Copyright (c) 2015 hacksalot / FluentDesk. @module fresh-theme.js
@module theme.js @license MIT. See LICENSE.md for details.
*/ */
(function() { (function() {
var FS = require('fs') var FS = require('fs')
, extend = require('../utils/extend') , extend = require('../utils/extend')
, validator = require('is-my-json-valid') , validator = require('is-my-json-valid')
@ -15,20 +17,25 @@ Definition of the Theme class.
, pathExists = require('path-exists').sync , pathExists = require('path-exists').sync
, EXTEND = require('../utils/extend') , EXTEND = require('../utils/extend')
, moment = require('moment') , 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. The FRESHTheme class is a representation of a FRESH theme
@class Theme asset. See also: JRSTheme.
@class FRESHTheme
*/ */
function Theme() { function FRESHTheme() {
} }
/** /**
Open and parse the specified theme. Open and parse the specified theme.
*/ */
Theme.prototype.open = function( themeFolder ) { FRESHTheme.prototype.open = function( themeFolder ) {
this.folder = themeFolder; this.folder = themeFolder;
@ -38,18 +45,7 @@ Definition of the Theme class.
// Set up a formats hash for the theme // Set up a formats hash for the theme
var formatsHash = { }; var formatsHash = { };
// See if the theme has a package.json. If so, load it. // Load the theme
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
var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' ); var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' );
var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) ); var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) );
var that = this; var that = this;
@ -67,10 +63,6 @@ Definition of the Theme class.
formatsHash = loadImplicit.call( this ); 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 // Cache
this.formats = formatsHash; this.formats = formatsHash;
@ -80,20 +72,29 @@ Definition of the Theme class.
return this; return this;
}; };
/** /**
Determine if the theme supports the specified output format. Determine if the theme supports the specified output format.
*/ */
Theme.prototype.hasFormat = function( fmt ) { FRESHTheme.prototype.hasFormat = function( fmt ) {
return _.has( this.formats, fmt ); return _.has( this.formats, fmt );
}; };
/** /**
Determine if the theme supports the specified output format. Determine if the theme supports the specified output format.
*/ */
Theme.prototype.getFormat = function( fmt ) { FRESHTheme.prototype.getFormat = function( fmt ) {
return this.formats[ fmt ]; return this.formats[ fmt ];
}; };
/**
Load the theme implicitly, by scanning the theme folder for
files. TODO: Refactor duplicated code with loadExplicit.
*/
function loadImplicit() { function loadImplicit() {
// Set up a hash of formats supported by this theme. // 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, // 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 // containing info for each file. While we're doing that, also build up
// the formatsHash object. // 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, // If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format // 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. // compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) { if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-'); 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; isMajor = true;
} }
@ -165,9 +166,13 @@ Definition of the Theme class.
}); });
// Now, get all the CSS files... // 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 ) { .forEach(function( cssf ) {
// For each CSS file, get its corresponding HTML file
var idx = _.findIndex(fmts, function( fmt ) { var idx = _.findIndex(fmts, function( fmt ) {
return fmt && fmt.pre === cssf.pre && fmt.ext === 'html'; return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
}); });
@ -184,33 +189,39 @@ Definition of the Theme class.
return formatsHash; return formatsHash;
} }
/**
Load the theme explicitly, by following the 'formats' hash
in the theme's JSON settings file.
*/
function loadExplicit() { function loadExplicit() {
var that = this; // Housekeeping
// Set up a hash of formats supported by this theme.
var formatsHash = { }; var formatsHash = { };
// Establish the base theme folder
var tplFolder = PATH.join( this.folder, 'src' ); var tplFolder = PATH.join( this.folder, 'src' );
var act = null; var act = null;
var that = this;
// Iterate over all files in the theme folder, producing an array, fmts, // 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 // containing info for each file. While we're doing that, also build up
// the formatsHash object. // the formatsHash object.
var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) { var fmts = READFILES( tplFolder ).map( function( absPath ) {
act = null; act = null;
// If this file is mentioned in the theme's JSON file under "transforms" // If this file is mentioned in the theme's JSON file under "transforms"
var pathInfo = parsePath(absPath); var pathInfo = parsePath(absPath);
var absPathSafe = absPath.trim().toLowerCase(); var absPathSafe = absPath.trim().toLowerCase();
var outFmt = _.find( Object.keys( that.formats ), function( fmtKey ) { var outFmt = _.find(
var fmtVal = that.formats[ fmtKey ]; Object.keys( that.formats ),
return _.some( fmtVal.transform, function( fpath ) { function( fmtKey ) {
var absPathB = PATH.join( that.folder, fpath ).trim().toLowerCase(); var fmtVal = that.formats[ fmtKey ];
return absPathB === absPathSafe; return _.some( fmtVal.transform, function(fpath) {
var absPathB = PATH.join( that.folder, fpath )
.trim().toLowerCase();
return absPathB === absPathSafe;
});
}); });
});
if( outFmt ) { if( outFmt ) {
act = 'transform'; act = 'transform';
} }
@ -231,7 +242,7 @@ Definition of the Theme class.
// compact-[outputformat].[extension], for ex, compact-pdf.html. // compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) { if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-'); 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. // We should have a valid output format now.
@ -261,7 +272,11 @@ Definition of the Theme class.
}); });
// Now, get all the CSS files... // 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 ) { .forEach(function( cssf ) {
// For each CSS file, get its corresponding HTML file // For each CSS file, get its corresponding HTML file
var idx = _.findIndex(fmts, function( fmt ) { var idx = _.findIndex(fmts, function( fmt ) {
@ -279,12 +294,22 @@ Definition of the Theme class.
return formatsHash; return formatsHash;
} }
/**
Return a more friendly name for certain formats.
TODO: Refactor
*/
function friendlyName( val ) { function friendlyName( val ) {
val = val.trim().toLowerCase(); val = val.trim().toLowerCase();
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }; var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val; return friendly[val] || val;
} }
module.exports = Theme;
module.exports = FRESHTheme;
}()); }());

85
src/core/jrs-theme.js Normal file
View File

@ -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;
}());

View File

@ -1,6 +1,7 @@
/** /**
Status codes for HackMyResume. Status codes for HackMyResume.
@module status-codes.js @module status-codes.js
@license MIT. See LICENSE.MD for details.
*/ */
(function(){ (function(){
@ -15,7 +16,8 @@ Status codes for HackMyResume.
resumeNotFoundAlt: 6, resumeNotFoundAlt: 6,
inputOutputParity: 7, inputOutputParity: 7,
createNameMissing: 8, createNameMissing: 8,
wkhtmltopdf: 9 wkhtmltopdf: 9,
missingPackageJSON: 10
}; };
}()); }());

View File

@ -1,6 +1,6 @@
/** /**
Generic template helper definitions for FluentCV. Generic template helper definitions for HackMyResume / FluentCV.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
@module generic-helpers.js @module generic-helpers.js
*/ */
@ -114,6 +114,16 @@ Generic template helper definitions for FluentCV.
if (lhs || rhs) return options.fn(this); if (lhs || rhs) return options.fn(this);
}, },
/**
Conditional stylesheet link. Either display the link or embed the stylesheet
via <style></style> tag.
*/
styleSheet: function( file, options ) {
return ( this.opts.css === 'link') ?
'<link href="' + file + '" rel="stylesheet" type="text/css">' :
'<style>' + this.cssInfo.data + '</style>';
},
/** /**
Perform a generic comparison. Perform a generic comparison.
See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates

View File

@ -40,6 +40,7 @@ Definition of the HandlebarsGenerator class.
RAW: json, RAW: json,
filt: opts.filters, filt: opts.filters,
cssInfo: cssInfo, cssInfo: cssInfo,
opts: opts,
headFragment: opts.headFragment || '' headFragment: opts.headFragment || ''
}); });

View File

@ -11,7 +11,6 @@ Definition of the UnderscoreGenerator class.
var _ = require('underscore'); var _ = require('underscore');
/** /**
Perform template-based resume generation using Underscore.js. Perform template-based resume generation using Underscore.js.
@class UnderscoreGenerator @class UnderscoreGenerator
@ -32,6 +31,10 @@ Definition of the UnderscoreGenerator class.
// Strip {# comments #} // Strip {# comments #}
jst = jst.replace( delims.comment, ''); 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. // Compile and run the template. TODO: avoid unnecessary recompiles.
var compiled = _.template(jst); var compiled = _.template(jst);
var ret = compiled({ var ret = compiled({
@ -40,13 +43,15 @@ Definition of the UnderscoreGenerator class.
XML: require('xml-escape'), XML: require('xml-escape'),
RAW: json, RAW: json,
cssInfo: cssInfo, cssInfo: cssInfo,
headFragment: opts.headFragment || '' headFragment: opts.headFragment || '',
opts: opts,
h: helpers
}); });
return ret; return ret;
} }
}; };
}()); }());

View File

@ -1,7 +1,7 @@
/** /**
Definition of the BaseGenerator class. Definition of the BaseGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module base-generator.js @module base-generator.js
@license MIT. See LICENSE.md for details.
*/ */
(function() { (function() {

View File

@ -1,6 +1,6 @@
/** /**
Definition of the HtmlPngGenerator class. 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 @module html-png-generator.js
*/ */
@ -11,7 +11,7 @@ Definition of the HtmlPngGenerator class.
, HTML = require( 'html' ); , 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({ var HtmlPngGenerator = module.exports = TemplateGenerator.extend({
@ -19,24 +19,29 @@ Definition of the HtmlPngGenerator class.
this._super( 'png', 'html' ); this._super( 'png', 'html' );
}, },
/** invoke: function( rez, themeMarkup, cssInfo, opts ) {
Generate the binary PDF. //return YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 );
*/ },
onBeforeSave: function( info ) {
png( info.mk, info.outputFile ); generate: function( rez, f, opts ) {
return null; // halt further processing 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 ) { function png( markup, fOut ) {
// require('webshot')( markup , { encoding: 'binary', siteType: 'html' } )
require('webshot')( markup , { encoding: 'binary', siteType: 'html' } ) // .pipe( FS.createWriteStream( fOut ) );
.pipe( FS.createWriteStream( fOut ) ); require('webshot')( markup , fOut, { siteType: 'html' }, function(err) { } );
} }
}()); }());

View File

@ -1,6 +1,6 @@
/** /**
Definition of the TemplateGenerator class. Definition of the TemplateGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
@module template-generator.js @module template-generator.js
*/ */
@ -17,7 +17,8 @@ Definition of the TemplateGenerator class.
, MKDIRP = require('mkdirp') , MKDIRP = require('mkdirp')
, BaseGenerator = require( './base-generator' ) , BaseGenerator = require( './base-generator' )
, EXTEND = require('../utils/extend') , 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 @method invoke
@param rez A FreshResume object. @param rez A FreshResume object.
@param opts Generator options. @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 ) { 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. Given a theme title, load the corresponding theme.
*/ */
function themeFromMoniker() { function themeFromMoniker() {
// Verify the specified theme name/path // Verify the specified theme name/path
var tFolder = PATH.join( var tFolder = PATH.join(
parsePath( require.resolve('fluent-themes') ).dirname, parsePath( require.resolve('fresh-themes') ).dirname,
this.opts.theme 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 // Load the theme and format
return { return {

View File

@ -8,7 +8,8 @@ module.exports = {
Sheet: require('./core/fresh-resume'), Sheet: require('./core/fresh-resume'),
FRESHResume: require('./core/fresh-resume'), FRESHResume: require('./core/fresh-resume'),
JRSResume: require('./core/jrs-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'), FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./gen/html-generator'), HtmlGenerator: require('./gen/html-generator'),
TextGenerator: require('./gen/text-generator'), TextGenerator: require('./gen/text-generator'),

View File

@ -8,29 +8,34 @@ Internal resume generation logic for HackMyResume.
module.exports = function () { module.exports = function () {
var unused = require('./utils/string') var unused = require('./utils/string')
, PATH = require('path'); , PATH = require('path')
, FS = require('fs');
/** /**
Display help documentation. Display help documentation.
*/ */
function help() { function help() {
console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ) var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' );
.useful.bold ); console.log( manPage.useful.bold );
} }
/** /**
Internal module interface. Used by FCV Desktop and HMR. 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 { return {
verbs: { verbs: v,
generate: require('./verbs/generate'), alias: {
build: require('./verbs/generate'), generate: v.build,
validate: require('./verbs/validate'), create: v.build
convert: require('./verbs/convert'),
create: require('./verbs/create'),
new: require('./verbs/create'),
help: help
}, },
lib: require('./hackmyapi'), lib: require('./hackmyapi'),
options: require('./core/default-options'), options: require('./core/default-options'),

View File

@ -55,7 +55,7 @@ function main() {
// Get the action to be performed // Get the action to be performed
var params = a._.map( function(p){ return p.toLowerCase().trim(); }); var params = a._.map( function(p){ return p.toLowerCase().trim(); });
var verb = params[0]; var verb = params[0];
if( !FCMD.verbs[ verb ] ) { if( !FCMD.verbs[ verb ] && !FCMD.alias[ verb ] ) {
logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn); logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn);
return; return;
} }
@ -74,10 +74,9 @@ function main() {
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt ); var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 ); var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 );
( splitAt === -1 ) && src.length > 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 // 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', theme: args.t || 'modern',
format: args.f || 'FRESH', format: args.f || 'FRESH',
prettify: !noPretty, prettify: !noPretty,
silent: args.s || args.silent silent: args.s || args.silent,
css: args.css || 'embed'
}; };
} }

View File

@ -1,17 +1,29 @@
/**
Implementation of the 'generate' verb for HackMyResume.
@module generate.js
@license MIT. See LICENSE.md for details.
*/
(function() { (function() {
var PATH = require('path') var PATH = require('path')
, FS = require('fs') , FS = require('fs')
, parsePath = require('parse-filepath')
, MD = require('marked') , MD = require('marked')
, MKDIRP = require('mkdirp') , MKDIRP = require('mkdirp')
, EXTEND = require('../utils/extend')
, parsePath = require('parse-filepath')
, _opts = require('../core/default-options') , _opts = require('../core/default-options')
, FluentTheme = require('../core/theme') , FluentTheme = require('../core/fresh-theme')
, JRSTheme = require('../core/jrs-theme')
, ResumeFactory = require('../core/resume-factory') , ResumeFactory = require('../core/resume-factory')
, _ = require('underscore') , _ = require('underscore')
, _fmts = require('../core/default-formats') , _fmts = require('../core/default-formats')
, _err, _log, rez; , _err, _log, rez;
/** /**
Handle an exception. Handle an exception.
*/ */
@ -19,42 +31,29 @@
throw ex; throw ex;
} }
module.exports =
/** /**
Given a source JSON resume, a destination resume path, and a theme file, Given a source resume in FRESH or JRS format, a destination resume path, and a
generate 0..N resumes in the desired formats. theme file, generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json". @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 dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern". @param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override. @param logger Optional logging override.
*/ */
function generate( src, dst, opts, logger, errHandler ) { function build( src, dst, opts, logger, errHandler ) {
// Housekeeping...
_log = logger || console.log; _log = logger || console.log;
_err = errHandler || error; _err = errHandler || error;
//_opts = extend( true, _opts, opts ); //_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false; _opts.prettify = opts.prettify === true ? _opts.prettify : false;
_opts.css = opts.css;
// Verify the specified theme name/path // Load the theme...
var relativeThemeFolder = '../../node_modules/fluent-themes/themes'; var tFolder = verify_theme( _opts.theme );
var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme); var theTheme = load_theme( tFolder );
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 input resumes... // Load input resumes...
if( !src || !src.length ) { throw { fluenterror: 3 }; } if( !src || !src.length ) { throw { fluenterror: 3 }; }
@ -70,41 +69,29 @@
}); });
msg && _log(msg); msg && _log(msg);
// Expand output resumes... (can't use map() here) // Expand output resumes...
var targets = [], that = this; var targets = expand( dst, theTheme );
( (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) ) }]);
});
// Run the transformation! // 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 // 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. Generate a single target resume such as "out/rez.html" or "out/rez.doc".
@param f Full path to the destination resume to generate, for example, @param targInfo Information for the target resume.
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt". @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>|<\/p>\s*$/gi, ''); return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
} }
@ -114,32 +101,32 @@
, fName = PATH.basename(f, '.' + fType) , fName = PATH.basename(f, '.' + fType)
, theFormat; , 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 + _log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold + 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( theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; 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( // Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; // (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 ); var outFolder = PATH.dirname( f );
MKDIRP.sync( outFolder ); // Ensure dest folder exists; MKDIRP.sync( outFolder ); // Ensure dest folder exists;
// TODO: refactor // JSON Resume themes have a 'render' method that needs to be called
if( theme.render ) { if( theme.render ) {
var COPY = require('copy'); var COPY = require('copy');
var globs = [ /*'**',*/ '*.css', '*.js', '*.png', '*.jpg', '*.gif', '*.bmp' ]; 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; var consoleLog = console.log;
console.log = function() { }; console.log = function() { };
@ -169,9 +156,12 @@
// Save the file // Save the file
FS.writeFileSync( f, rezHtml ); FS.writeFileSync( f, rezHtml );
// Return markup to the client
return rezHtml;
} }
else { 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;
}()); }());

View File

@ -9,59 +9,54 @@ var chai = require('chai')
chai.config.includeStack = false; 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 () { var _sheet;
function tryOpen() {
_sheet = new FRESHResume().open(
'node_modules/jane-q-fullstacker/resume/jane-resume.json' );
}
tryOpen.should.not.Throw();
});
it('should have one or more of each section', function() { it('should open without throwing an exception', function () {
expect( function tryOpen() {
//(_sheet.basics) && _sheet = new FRESHResume().open( opts.path );
_sheet.name && _sheet.info && _sheet.location && _sheet.contact && }
(_sheet.employment.history && _sheet.employment.history.length > 0) && tryOpen.should.not.Throw();
(_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 have a work duration of 7 years', function() { it('should have one or more of each section', function() {
_sheet.computed.numYears.should.equal( 7 ); var newObj = _.pick( _sheet, opts.sections );
}); expect( Object.keys(newObj).length ).to.equal( opts.sections.length );
});
it('should save without throwing an exception', function(){ it('should have a work duration of ' + opts.duration + ' years', function() {
function trySave() { _sheet.computed.numYears.should.equal( opts.duration );
_sheet.save( 'test/sandbox/jane-q-fullstacker.json' ); });
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() { it('should save without throwing an exception', function(){
var savedSheet = new FRESHResume().open('test/sandbox/jane-q-fullstacker.json'); function trySave() {
_sheet.stringify().should.equal( savedSheet.stringify() ); _sheet.save( 'test/sandbox/' + opts.title + '.json' );
}); }
trySave.should.not.Throw();
});
it('should validate against the FRESH resume schema', function() { it('should not be modified after saving', function() {
var result = _sheet.isValid(); var savedSheet = new FRESHResume().open('test/sandbox/' + opts.title + '.json');
// var schemaJson = require('fresca'); _sheet.stringify().should.equal( savedSheet.stringify() );
// var validate = validator( schemaJson, { verbose: true } ); });
// var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " + it('should validate against the FRESH resume schema', function() {
"Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n"); var result = _sheet.isValid();
result.should.equal( true ); // 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 });

View File

@ -12,7 +12,9 @@ var SPAWNWATCHER = require('../src/core/spawn-watch')
chai.config.includeStack = false; chai.config.includeStack = false;
describe('Testing themes', function () { function genThemes( title, src, fmt ) {
describe('Testing themes against ' + title.toUpperCase() + ' resume ' + '(' + fmt + ')' , function () {
var _sheet; var _sheet;
@ -29,11 +31,11 @@ describe('Testing themes', function () {
function genTheme( fmt, src, themeName, themeLoc, testTitle ) { function genTheme( fmt, src, themeName, themeLoc, testTitle ) {
themeLoc = themeLoc || themeName; 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 () { it( testTitle, function () {
function tryOpen() { function tryOpen() {
//var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; //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 = { var opts = {
theme: themeLoc, theme: themeLoc,
format: fmt, format: fmt,
@ -46,28 +48,21 @@ describe('Testing themes', function () {
}); });
} }
var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; genTheme(fmt, src, 'hello-world');
genTheme('FRESH', src, 'hello-world'); genTheme(fmt, src, 'compact');
genTheme('FRESH', src, 'compact'); genTheme(fmt, src, 'modern');
genTheme('FRESH', src, 'modern'); genTheme(fmt, src, 'minimist');
genTheme('FRESH', src, 'minimist'); genTheme(fmt, src, 'awesome');
genTheme('FRESH', src, 'awesome'); genTheme(fmt, src, 'positive');
genTheme('FRESH', src, 'positive'); genTheme(fmt, src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' );
genTheme('FRESH', src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' ); genTheme(fmt, src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' );
genTheme('FRESH', src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' ); genTheme(fmt, src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' );
genTheme('FRESH', src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' ); genTheme(fmt, src, 'jsonresume-theme-classy', 'node_modules/jsonresume-theme-classy' );
genTheme('FRESH', 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' );