1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-07-04 09: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-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

View File

@ -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",

View File

@ -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;

View File

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

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

View File

@ -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 <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.
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,
filt: opts.filters,
cssInfo: cssInfo,
opts: opts,
headFragment: opts.headFragment || ''
});

View File

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

View File

@ -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() {

View File

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

View File

@ -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 {

View File

@ -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'),

View File

@ -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'),

View File

@ -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'
};
}

View File

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

View File

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

View File

@ -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' );