1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2025-05-12 00:27:08 +01:00

Compare commits

...

23 Commits

Author SHA1 Message Date
78a8b9c58e Merge pull request #40 from hacksalot/rel/v1.2.2
rel/v1.2.2
2015-12-24 17:44:56 -05:00
5e7abb66bd Safer source format conversions.
Quick fix against missing fields in FRESH and/or JRS (ahead of introing
more robust standalone converter thing). Address portions of #31 and
#33.
2015-12-24 17:51:26 -05:00
358c397bb9 Show call stack on error.
Hat tip @Furchin.
2015-12-24 16:22:29 -05:00
3d41528059 Fix path parsing issue on prev versions of Node.js.
Work around absence of path.parse in Node versions < v0.12. Addresses
#31 and #33.
2015-12-24 16:18:38 -05:00
79637b611a Bump version to 1.2.2. 2015-12-24 16:09:37 -05:00
5de796b119 Merge pull request #35 from hacksalot/rel/v1.2.1
rel/v1.2.1
2015-12-24 07:41:12 -05:00
bf84341acf Version -> 1.2.1
Holding off on 1.3.0 pending HTTP/HTTPS support. #rebase #semver
2015-12-24 07:42:07 -05:00
bbac1fdceb Improve test coverage around incomplete JRS resumes.
Add quick sanity checks around incomplete or irregular JRS-format
resumes. Also decorate existing JRS test resumes (/tests/resumes/) with
current 0.0.0 JRS version ahead of 1.0.0 release.
2015-12-24 06:08:45 -05:00
c5ee1ee33c Quick fix for ".history" errors.
Affects #31 and #33.
2015-12-24 04:05:56 -05:00
c74eda90ed Introduce lodash dependency. 2015-12-24 04:05:17 -05:00
ef2fe95bd8 Remove unused method. 2015-12-24 04:04:44 -05:00
e2589b3730 Fix validate command error.
Still hitting some inconsistent behavior in different NPM
versions/platforms with invalid uppercase dependency names per
https://github.com/npm/npm/issues/3692. Partial fix for #33.
2015-12-24 03:23:56 -05:00
ebad1677bc Replace file-exists.js with NPM path-exists. 2015-12-22 18:55:17 -05:00
dab6ebfd82 Bump fluent-themes version to 0.8.0-beta. 2015-12-22 18:49:29 -05:00
dd61b5360a Update package.json contact info. 2015-12-22 18:49:11 -05:00
fced92a5a0 Bump version to 1.3.0. 2015-12-22 18:47:23 -05:00
64db1a654e Merge pull request #30 from hacksalot/rel/v1.2.0
rel/v1.2.0
2015-12-21 02:57:59 -05:00
31830ee759 Silence tests. 2015-12-21 03:04:40 -05:00
1c05846a4f Add CLI tests. 2015-12-21 02:58:16 -05:00
1db9c2e420 Fix README glitch. 2015-12-21 02:58:01 -05:00
c966f6766c Refactor verbs to separate files. 2015-12-21 02:56:02 -05:00
65b6359fd8 Bump version to 1.2.0. 2015-12-21 00:52:23 -05:00
a54476eede Reaffirm string-based generation.
In recent commits, HackMyResume generation logic, much like the pilots
in Top Gun who became too reliant on air-to-air missiles and lost the
true art of dogfighting, has become dependent on file-based generation
as implicit file assumptions have crept in. This commit reaffirms the
file-less, string-based nature of the generation process and, as a side
effect, adjusts the behavior of (binary) PDF generation to match.
2015-12-21 00:36:08 -05:00
29 changed files with 792 additions and 448 deletions

View File

@ -48,7 +48,7 @@ To use HackMyResume you'll need to create a valid resume in either
[FRESH][fresca] or [JSON Resume][6] format. Then you can start using the command
line tool. There are four basic commands you should be aware of:
- `**build**` generates resumes in HTML, Word, Markdown, PDF, and other formats.
- **build** generates resumes in HTML, Word, Markdown, PDF, and other formats.
Use it when you need to submit, upload, print, or email resumes in specific
formats.
@ -58,7 +58,7 @@ formats.
hackmyresume BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all
```
- `**new**` creates a new resume in FRESH or JSON Resume format.
- **new** creates a new resume in FRESH or JSON Resume format.
```bash
# hackmyresume NEW <OUTPUTS> [-f <FORMAT>]
@ -67,7 +67,7 @@ formats.
hackmyresume NEW r1.json r2.json -f jrs
```
- `**convert**` converts your source resume between FRESH and JSON Resume
- **convert** converts your source resume between FRESH and JSON Resume
formats.
Use it to convert between the two formats to take advantage of tools and
services.
@ -78,7 +78,7 @@ services.
hackmyresume CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json
```
- `**validate**` validates the specified resume against either the FRESH or JSON
- **validate** validates the specified resume against either the FRESH or JSON
Resume schema. Use it to make sure your resume data is sufficient and complete.
```bash

View File

@ -1,6 +1,6 @@
{
"name": "hackmyresume",
"version": "1.1.0",
"version": "1.2.2",
"description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.",
"repository": {
"type": "git",
@ -24,7 +24,7 @@
"Underscore",
"template"
],
"author": "hacksalot <hacksalot@fluentdesk.com> (https://github.com/hacksalot)",
"author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
"license": "MIT",
"preferGlobal": "true",
"bugs": {
@ -37,17 +37,20 @@
"homepage": "https://github.com/hacksalot/HackMyResume",
"dependencies": {
"colors": "^1.1.2",
"fluent-themes": "~0.7.0-beta",
"fluent-themes": "~0.7.1-beta",
"fresca": "~0.2.2",
"fs-extra": "^0.24.0",
"handlebars": "^4.0.5",
"html": "0.0.10",
"is-my-json-valid": "^2.12.2",
"jst": "0.0.13",
"lodash": "^3.10.1",
"marked": "^0.3.5",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"moment": "^2.10.6",
"parse-filepath": "^0.6.3",
"path-exists": "^2.1.0",
"recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.2.0",
"underscore": "^1.8.3",

View File

@ -16,6 +16,7 @@ FRESH to JSON Resume conversion routiens.
/**
Convert from JSON Resume format to FRESH.
@method toFresh
@todo Refactor
*/
toFRESH: function( src, foreign ) {
@ -67,6 +68,7 @@ FRESH to JSON Resume conversion routiens.
Convert from FRESH format to JSON Resume.
@param foreign True if non-JSON-Resume properties should be included in
the result, false if those properties should be excluded.
@todo Refactor
*/
toJRS: function( src, foreign ) {
@ -112,6 +114,7 @@ FRESH to JSON Resume conversion routiens.
};
function meta( direction, obj ) {
if( !obj ) return obj; // preserve null and undefined
if( direction ) {
obj = obj || { };
obj.format = obj.format || "FRESH@0.1.0";
@ -121,6 +124,7 @@ FRESH to JSON Resume conversion routiens.
}
function employment( obj, direction ) {
if( !obj ) return obj;
if( !direction ) {
return obj && obj.history ?
obj.history.map(function(emp){
@ -157,6 +161,7 @@ FRESH to JSON Resume conversion routiens.
function education( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj && obj.length ? {
history: obj.map(function(edu){
@ -191,6 +196,7 @@ FRESH to JSON Resume conversion routiens.
}
function service( obj, direction, foreign ) {
if( !obj ) return obj;
if( direction ) {
return {
history: obj && obj.length ? obj.map(function(vol) {
@ -225,6 +231,7 @@ FRESH to JSON Resume conversion routiens.
}
function social( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj.map(function(pro){
return {
@ -247,6 +254,7 @@ FRESH to JSON Resume conversion routiens.
}
function recognition( obj, direction, foreign ) {
if( !obj ) return obj;
if( direction ) {
return obj && obj.length ? obj.map(
function(awd){
@ -275,6 +283,7 @@ FRESH to JSON Resume conversion routiens.
}
function references( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj && obj.length && obj.map(function(ref){
return {
@ -296,6 +305,7 @@ FRESH to JSON Resume conversion routiens.
}
function writing( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj.map(function( pub ) {
return {

View File

@ -0,0 +1,19 @@
(function(){
var FLUENT = require('../hackmyapi');
/**
Supported resume formats.
*/
module.exports = [
{ name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() },
{ name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() },
{ name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() }
];
}());

View File

@ -0,0 +1,13 @@
(function(){
module.exports = {
theme: 'modern',
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
unformatted: ['em','strong'],
max_char: 80, // ← See lib/html.js in above-linked repo
//wrap_line_length: 120, ← Don't use this
}
};
}());

View File

@ -10,6 +10,7 @@ Definition of the FRESHResume class.
, extend = require('../utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, __ = require('lodash')
, PATH = require('path')
, moment = require('moment')
, MD = require('marked')
@ -187,15 +188,6 @@ Definition of the FRESHResume class.
return flatSkills;
},
/**
Update the sheet's raw data. TODO: remove/refactor
*/
FreshResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str );
return this;
};
/**
Reset the sheet to an empty state.
*/
@ -299,7 +291,7 @@ Definition of the FRESHResume class.
Validate the sheet against the FRESH Resume schema.
*/
FreshResume.prototype.isValid = function( info ) {
var schemaObj = require('FRESCA');
var schemaObj = require('fresca');
var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
@ -321,7 +313,8 @@ Definition of the FRESHResume class.
sheets that have overlapping jobs.
*/
FreshResume.prototype.duration = function() {
if( this.employment.history && this.employment.history.length ) {
var empHist = __.get(this, 'employment.history');
if( empHist && empHist.length ) {
var firstJob = _.last( this.employment.history );
var careerStart = firstJob.start ? firstJob.safe.start : '';
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
@ -341,9 +334,9 @@ Definition of the FRESHResume class.
*/
FreshResume.prototype.sort = function( ) {
this.employment.history && this.employment.history.sort( byDateDesc );
this.education.history && this.education.history.sort( byDateDesc );
this.service.history && this.service.history.sort( byDateDesc );
__.get(this, 'employment.history') && this.employment.history.sort( byDateDesc );
__.get(this, 'education.history') && this.education.history.sort( byDateDesc );
__.get(this, 'service.history') && this.service.history.sort( byDateDesc );
// this.awards && this.awards.sort( function(a, b) {
// return( a.safeDate.isBefore(b.safeDate) ) ? 1

View File

@ -107,15 +107,6 @@ Definition of the JRSResume class.
return flatSkills;
};
/**
Update the sheet's raw data. TODO: remove/refactor
*/
JRSResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str );
return this;
};
/**
Reset the sheet to an empty state.
*/

View File

@ -0,0 +1,13 @@
(function(){
var FRESHResume = require('../core/fresh-resume');
module.exports = function loadSourceResumes( src, log, fn ) {
return src.map( function( res ) {
log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info +
res.cyan.bold );
return (fn && fn(res)) || (new FRESHResume()).open( res );
});
};
}());

View File

@ -11,6 +11,7 @@ Definition of the Theme class.
, validator = require('is-my-json-valid')
, _ = require('underscore')
, PATH = require('path')
, parsePath = require('parse-filepath')
, EXTEND = require('../utils/extend')
, moment = require('moment')
, RECURSIVE_READ_DIR = require('recursive-readdir-sync');
@ -30,8 +31,8 @@ Definition of the Theme class.
// Open the [theme-name].json file; should have the same name as folder
this.folder = themeFolder;
var pathInfo = PATH.parse( themeFolder );
var themeFile = PATH.join( themeFolder, pathInfo.base + '.json' );
var pathInfo = parsePath( themeFolder );
var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' );
var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) );
var that = this;
@ -59,7 +60,7 @@ Definition of the Theme class.
this.formats = formatsHash;
// Set the official theme name
this.name = PATH.parse( this.folder ).name;
this.name = parsePath( this.folder ).name;
return this;
};
@ -96,9 +97,9 @@ Definition of the Theme class.
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
var pathInfo = PATH.parse(absPath);
var pathInfo = parsePath(absPath);
var outFmt = '', isMajor = false;
var portion = pathInfo.dir.replace(tplFolder,'');
var portion = pathInfo.dirname.replace(tplFolder,'');
if( portion && portion.trim() ) {
if( portion[1] === '_' ) return;
var reg = /^(?:\/|\\)(html|latex|doc|pdf|partials)(?:\/|\\)?/ig;
@ -134,8 +135,8 @@ Definition of the Theme class.
action: 'transform',
path: absPath,
major: isMajor,
orgPath: PATH.relative(that.folder, absPath),
ext: pathInfo.ext.slice(1),
orgPath: PATH.relative(tplFolder, absPath),
ext: pathInfo.extname.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
@ -186,7 +187,7 @@ Definition of the Theme class.
act = null;
// If this file is mentioned in the theme's JSON file under "transforms"
var pathInfo = PATH.parse(absPath);
var pathInfo = parsePath(absPath);
var absPathSafe = absPath.trim().toLowerCase();
var outFmt = _.find( Object.keys( that.formats ), function( fmtKey ) {
var fmtVal = that.formats[ fmtKey ];
@ -203,7 +204,7 @@ Definition of the Theme class.
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
if( !outFmt ) {
var portion = pathInfo.dir.replace(tplFolder,'');
var portion = pathInfo.dirname.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
var res = reg.exec( portion );
@ -231,7 +232,7 @@ Definition of the Theme class.
action: act,
orgPath: PATH.relative(that.folder, absPath),
path: absPath,
ext: pathInfo.ext.slice(1),
ext: pathInfo.extname.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,

View File

@ -161,7 +161,7 @@ Generic template helper definitions for FluentCV.
}
else {
idx = Math.min( lvl / 2, 4 );
idx = Math.max( 0, intVal );
idx = Math.max( 0, idx );
}
return idx;
}

View File

@ -22,16 +22,6 @@ Definition of the HTMLGenerator class.
the HTML resume prior to saving.
*/
onBeforeSave: function( info ) {
var cssSrc = PATH.join( info.theme.folder, 'src', '*.css' )
, outFolder = PATH.parse( info.outputFile ).dir, that = this;
info.theme.cssFiles.forEach( function( f ) {
var fi = PATH.parse( f.path );
FS.copySync( f.path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) {
throw { fluenterror: that.codes.copyCss, data: [cssSrc,cssDst] };
});
});
return this.opts.prettify ?
HTML.prettyPrint( info.mk, this.opts.prettify ) : info.mk;
}

View File

@ -23,8 +23,8 @@ Definition of the HtmlPdfGenerator class.
Generate the binary PDF.
*/
onBeforeSave: function( info ) {
pdf(info.mk, info.outputFile);
return info.mk;
pdf( info.mk, info.outputFile );
return null; // halt further processing
}
});

View File

@ -13,6 +13,7 @@ Definition of the TemplateGenerator class.
, MD = require( 'marked' )
, XML = require( 'xml-escape' )
, PATH = require('path')
, parsePath = require('parse-filepath')
, MKDIRP = require('mkdirp')
, BaseGenerator = require( './base-generator' )
, EXTEND = require('../utils/extend')
@ -38,7 +39,9 @@ Definition of the TemplateGenerator class.
raw: function( txt ) { return txt; },
xml: function( txt ) { return XML(txt); },
md: function( txt ) { return MD( txt || '' ); },
mdin: function( txt ) { return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); },
mdin: function( txt ) {
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
},
lower: function( txt ) { return txt.toLowerCase(); },
link: function( name, url ) { return url ?
'<a href="' + url + '">' + name + '</a>' : name; }
@ -69,24 +72,14 @@ Definition of the TemplateGenerator class.
},
invoke: function( rez, themeMarkup, cssInfo, opts ) {
this.opts = EXTEND( true, {}, _defaultOpts, opts );
mk = this.single( rez, themeMarkup, this.format, cssInfo, { } );
this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f ));
return mk;
},
/**
Default generation method for template-based generators.
@method generate
String-based template generation method.
@method invoke
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options.
@returns An array of strings representing generated output files.
*/
generate: function( rez, f, opts ) {
invoke: function( rez, opts ) {
// Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts );
@ -96,20 +89,81 @@ Definition of the TemplateGenerator class.
var theme = themeInfo.theme;
var tFolder = themeInfo.folder;
var tplFolder = PATH.join( tFolder, 'src' );
var outFolder = PATH.parse(f).dir;
var curFmt = theme.getFormat( this.format );
var that = this;
// "Generate": process individual files within the theme
curFmt.files.forEach(function(tplInfo){
if( tplInfo.action === 'transform' ) {
transform.call( that, rez, f, tplInfo, theme, outFolder );
return {
files: curFmt.files.map( function( tplInfo ) {
return {
info: tplInfo,
data: tplInfo.action === 'transform' ?
transform.call( that, rez, tplInfo, theme ) : undefined
};
}).filter(function(item){ return item !== null; }),
themeInfo: themeInfo
};
},
/**
File-based template generation method.
@method generate
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options.
*/
generate: function( rez, f, opts ) {
// Call the generation method
var genInfo = this.invoke( rez, opts );
// Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts );
// Load the theme
var themeInfo = genInfo.themeInfo;
var theme = themeInfo.theme;
var tFolder = themeInfo.folder;
var tplFolder = PATH.join( tFolder, 'src' );
var outFolder = parsePath(f).dirname;
var curFmt = theme.getFormat( this.format );
var that = this;
// "Generate": process individual files within the theme
genInfo.files.forEach(function( file ){
var thisFilePath;
if( file.info.action === 'transform' ) {
thisFilePath = PATH.join( outFolder, file.info.orgPath );
try {
if( that.onBeforeSave ) {
file.data = that.onBeforeSave({
theme: theme,
outputFile: (file.info.major ? f : thisFilePath),
mk: file.data
});
if( !file.data ) return; // PDF etc
}
var fileName = file.info.major ? f : thisFilePath;
MKDIRP.sync( PATH.dirname( fileName ) );
FS.writeFileSync( fileName, file.data,
{ encoding: 'utf8', flags: 'w' } );
that.onAfterSave && that.onAfterSave(
{ outputFile: fileName, mk: file.data } );
}
catch(ex) {
console.log(ex);
}
}
else if( tplInfo.action === null && theme.explicit ) {
var thisFilePath = PATH.join(outFolder, tplInfo.orgPath);
else if( file.info.action === null/* && theme.explicit*/ ) {
thisFilePath = PATH.join( outFolder, file.info.orgPath );
try {
MKDIRP.sync( PATH.dirname(thisFilePath) );
FS.copySync( tplInfo.path, thisFilePath );
FS.copySync( file.info.path, thisFilePath );
}
catch(ex) {
console.log(ex);
@ -123,7 +177,7 @@ Definition of the TemplateGenerator class.
var absLoc = PATH.join(outFolder, loc);
var absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
// 'file', 'dir', or 'junction' (Windows only)
var type = PATH.parse( absLoc ).ext ? 'file' : 'junction';
var type = parsePath( absLoc ).extname ? 'file' : 'junction';
FS.symlinkSync( absTarg, absLoc, type);
});
}
@ -142,8 +196,10 @@ Definition of the TemplateGenerator class.
*/
single: function( json, jst, format, cssInfo, opts, theme ) {
this.opts.freezeBreaks && ( jst = freeze(jst) );
var eng = require( '../eng/' + theme.engine + '-generator' );
var result = eng.generate( json, jst, format, cssInfo, opts, theme );
this.opts.freezeBreaks && ( result = unfreeze(result) );
return result;
}
@ -166,10 +222,10 @@ Definition of the TemplateGenerator class.
function themeFromMoniker() {
// Verify the specified theme name/path
var tFolder = PATH.join(
PATH.parse( require.resolve('fluent-themes') ).dir,
parsePath( require.resolve('fluent-themes') ).dirname,
this.opts.theme
);
var exists = require('../utils/file-exists');
var exists = require('path-exists').sync;
if( !exists( tFolder ) ) {
tFolder = PATH.resolve( this.opts.theme );
if( !exists( tFolder ) ) {
@ -188,18 +244,15 @@ Definition of the TemplateGenerator class.
/**
Transform a single subfile.
*/
function transform( rez, f, tplInfo, theme, outFolder ) {
var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null };
var mk = this.single( rez, tplInfo.data, this.format, cssInfo, this.opts, theme );
this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } ));
var thisFilePath = PATH.join( outFolder, tplInfo.orgPath );
function transform( rez, tplInfo, theme ) {
try {
MKDIRP.sync( PATH.dirname( tplInfo.major ? f : thisFilePath) );
FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } );
this.onAfterSave && (mk = this.onAfterSave( { outputFile: (tplInfo.major ? f : thisFilePath), mk: mk } ));
var cssInfo = {
file: tplInfo.css ? tplInfo.cssPath : null,
data: tplInfo.css || null
};
return this.single( rez, tplInfo.data, this.format, cssInfo, this.opts,
theme );
}
catch(ex) {
console.log(ex);

View File

@ -7,279 +7,9 @@ Internal resume generation logic for HackMyResume.
(function() {
module.exports = function () {
var path = require( 'path' )
, extend = require( './utils/extend' )
, unused = require('./utils/string')
, FS = require('fs')
, _ = require('underscore')
, FLUENT = require('./hackmyapi')
, PATH = require('path')
, MKDIRP = require('mkdirp')
//, COLORS = require('colors')
, rez, _log, _err;
var unused = require('./utils/string')
, PATH = require('path');
/**
Given a source JSON resume, a destination resume path, and a theme file,
generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
*/
function generate( src, dst, opts, logger, errHandler ) {
_log = logger || console.log;
_err = errHandler || error;
//_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
// Load input resumes...
if(!src || !src.length) { throw { fluenterror: 3 }; }
var sheets = loadSourceResumes( src );
// Merge input resumes...
var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ?
'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName;
return extend( true, b, a );
});
msg && _log(msg);
// Verify the specified theme name/path
var relativeThemeFolder = '../node_modules/fluent-themes/themes';
var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme);
var exists = require('./utils/file-exists');
if (!exists( tFolder )) {
tFolder = PATH.resolve( _opts.theme );
if (!exists( tFolder )) {
throw { fluenterror: 1, data: _opts.theme };
}
}
// Load the theme
var theTheme = new FLUENT.Theme().open( tFolder );
_opts.themeObj = theTheme;
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
(' theme (' +Object.keys(theTheme.formats).length + ' formats)').info);
// Expand output resumes... (can't use map() here)
var targets = [], that = this;
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) {
var to = path.resolve(t),
pa = path.parse(to),
fmat = pa.ext || '.all';
targets.push.apply(targets, fmat === '.all' ?
Object.keys( theTheme.formats ).map(function(k){
var z = theTheme.formats[k];
return { file: to.replace(/all$/g,z.outFormat), fmt: z };
}) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
});
// Run the transformation!
var finished = targets.map( function(t) { return single(t, theTheme); });
// Don't send the client back empty-handed
return { sheet: rez, targets: targets, processed: finished };
}
/**
Generate a single resume of a specific format.
@param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function single( targInfo, theme ) {
try {
var f = targInfo.file
, fType = targInfo.fmt.outFormat
, fName = path.basename(f, '.' + fType)
, theFormat;
// If targInfo.fmt.files exists, this theme has an explicit "files"
// section in its theme.json file.
if( targInfo.fmt.files && targInfo.fmt.files.length ) {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + path.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
// targInfo.fmt.files.forEach( function( form ) {
//
// if( form.action === 'transform' ) {
// var theFormat = _fmts.filter( function( fmt ) {
// return fmt.name === targInfo.fmt.outFormat;
// })[0];
// MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
// theFormat.gen.generate( rez, f, _opts );
// }
// else if( form.action === null ) {
// // Copy the file
// }
//
// });
}
// Otherwise the theme has no files section
else {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + path.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
}
}
catch( ex ) {
_err( ex );
}
}
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
/**
Validate 1 to N resumes in either FRESH or JSON Resume format.
*/
function validate( src, unused, opts, logger ) {
_log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
var isValid = true;
var validator = require('is-my-json-valid');
var schemas = {
fresh: require('FRESCA'),
jars: require('./core/resume.json')
};
// Load input resumes...
var sheets = loadSourceResumes(src, function( res ) {
try {
return {
file: res,
raw: FS.readFileSync( res, 'utf8' )
};
}
catch( ex ) {
throw ex;
}
});
sheets.forEach( function( rep ) {
var rez;
try {
rez = JSON.parse( rep.raw );
}
catch( ex ) {
_log('Validating '.info + rep.file.infoBold +
' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold);
if (ex instanceof SyntaxError) {
// Invalid JSON
_log( '--> '.bold.red + rep.file.toUpperCase().red +
' contains invalid JSON. Unable to validate.'.red );
_log( (' INTERNAL: ' + ex).red );
}
else {
_log(('ERROR: ' + ex.toString()).red.bold);
}
return;
}
var isValid = false;
var style = 'useful';
var errors = [];
var fmt = rez.meta &&
(rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars';
try {
var validate = validator( schemas[ fmt ], { // Note [1]
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
isValid = validate( rez );
if( !isValid ) {
style = 'warn';
errors = validate.errors;
}
}
catch(ex) {
}
_log( 'Validating '.info + rep.file.infoBold + ' against '.info +
fmt.replace('jars','JSON Resume').toUpperCase().infoBold +
' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold );
errors.forEach(function(err,idx) {
_log( '--> '.bold.yellow +
(err.field.replace('data.','resume.').toUpperCase() + ' ' +
err.message).yellow );
});
});
}
/**
Convert between FRESH and JRS formats.
*/
function convert( src, dst, opts, logger ) {
_log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
if( !dst || !dst.length ) {
if( src.length === 1 ) { throw { fluenterror: 5 }; }
else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; }
else { throw { fluenterror: 5 }; }
}
if( src && dst && src.length && dst.length && src.length !== dst.length ) {
throw { fluenterror: 7 };
}
var sheets = loadSourceResumes( src );
sheets.forEach(function(sheet, idx){
var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH';
var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS';
_log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' +
sourceFormat + ') to ').useful + dst[0].useful.bold +
(' (' + targetFormat + ').').useful );
sheet.saveAs( dst[idx], targetFormat );
});
}
/**
Create a new empty resume in either FRESH or JRS format.
*/
function create( src, dst, opts, logger ) {
_log = logger || console.log;
dst = src || ['resume.json'];
dst.forEach( function( t ) {
var safeFormat = opts.format.toUpperCase();
_log('Creating new '.useful +safeFormat.useful.bold +
' resume: '.useful + t.useful.bold);
MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists;
FLUENT[ safeFormat + 'Resume' ].default().save( t );
});
}
/**
Display help documentation.
@ -289,55 +19,22 @@ Internal resume generation logic for HackMyResume.
.useful.bold );
}
function loadSourceResumes( src, fn ) {
return src.map( function( res ) {
_log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info +
res.cyan.bold );
return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res );
});
}
/**
Supported resume formats.
*/
var _fmts = [
{ name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() },
{ name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() },
{ name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() }
];
/**
Default HackMyResume options.
*/
var _opts = {
theme: 'modern',
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
unformatted: ['em','strong'],
max_char: 80, // ← See lib/html.js in above-linked repo
//wrap_line_length: 120, ← Don't use this
}
};
/**
Internal module interface. Used by FCV Desktop and HMR.
*/
return {
verbs: {
build: generate,
validate: validate,
convert: convert,
new: create,
generate: require('./verbs/generate'),
build: require('./verbs/generate'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
create: require('./verbs/create'),
new: require('./verbs/create'),
help: help
},
lib: require('./hackmyapi'),
options: _opts,
formats: _fmts
options: require('./core/default-options'),
formats: require('./core/default-formats')
};
}();

View File

@ -68,7 +68,7 @@ function main() {
// Massage inputs and outputs
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 );
( splitAt === -1 ) && dst.push( src.pop() ); // Allow omitting TO keyword
( splitAt === -1 ) && src.length > 1 && dst.push( src.pop() ); // Allow omitting TO keyword
var parms = [ src, dst, opts, logMsg ];
// Invoke the action
@ -91,6 +91,7 @@ function getOpts( args ) {
};
}
// TODO: refactor
function handleError( ex ) {
var msg = '', exitCode;
@ -108,9 +109,10 @@ function handleError( ex ) {
}).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold;
break;
//case 4: msg = title + '\n' + ; break;
case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break;
case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created.'.guide; break;
case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break;
case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break;
case 8: msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold + ' to create.'.guide; break;
}
exitCode = ex.fluenterror;
@ -122,8 +124,10 @@ function handleError( ex ) {
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
if( !ex.fluenterror || ex.fluenterror < 3 )
if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s
console.log( ('ERROR: ' + trimmed.toString()).red.bold );
console.log( ex.stack.gray);
}
else
console.log( trimmed.toString() );

View File

@ -1,19 +0,0 @@
/**
Definition of the `fileExists` method.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module file-exists.js
*/
var FS = require('fs');
// Yup, this is now the recommended way to check for file existence on Node.
// fs.exists is deprecated and the recommended fs.statSync/lstatSync throws
// exceptions on non-existent paths :)
module.exports = function (path) {
try {
FS.statSync( path );
return true;
} catch( err ) {
return !(err && err.code === 'ENOENT');
}
};

30
src/verbs/convert.js Normal file
View File

@ -0,0 +1,30 @@
(function(){
var loadSourceResumes = require('../core/load-source-resumes');
/**
Convert between FRESH and JRS formats.
*/
module.exports = function convert( src, dst, opts, logger ) {
var _log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
if( !dst || !dst.length ) {
if( src.length === 1 ) { throw { fluenterror: 5 }; }
else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; }
else { throw { fluenterror: 5 }; }
}
if( src && dst && src.length && dst.length && src.length !== dst.length ) {
throw { fluenterror: 7 };
}
var sheets = loadSourceResumes( src, _log );
sheets.forEach(function(sheet, idx){
var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH';
var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS';
_log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' +
sourceFormat + ') to ').useful + dst[0].useful.bold +
(' (' + targetFormat + ').').useful );
sheet.saveAs( dst[idx], targetFormat );
});
};
}());

22
src/verbs/create.js Normal file
View File

@ -0,0 +1,22 @@
(function(){
var FLUENT = require('../hackmyapi')
, MKDIRP = require('mkdirp')
, PATH = require('path');
/**
Create a new empty resume in either FRESH or JRS format.
*/
module.exports = function create( src, dst, opts, logger ) {
var _log = logger || console.log;
if( !src || !src.length ) throw { fluenterror: 8 };
src.forEach( function( t ) {
var safeFormat = opts.format.toUpperCase();
_log('Creating new '.useful +safeFormat.useful.bold +
' resume: '.useful + t.useful.bold);
MKDIRP.sync( PATH.dirname( t ) ); // Ensure dest folder exists;
FLUENT[ safeFormat + 'Resume' ].default().save( t );
});
};
}());

150
src/verbs/generate.js Normal file
View File

@ -0,0 +1,150 @@
(function() {
var PATH = require('path')
, parsePath = require('parse-filepath')
, MKDIRP = require('mkdirp')
, _opts = require('../core/default-options')
, FluentTheme = require('../core/theme')
, loadSourceResumes = require('../core/load-source-resumes')
, _ = require('underscore')
, _fmts = require('../core/default-formats')
, _err, _log, rez;
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
module.exports =
/**
Given a source JSON resume, a destination resume path, and a theme file,
generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
*/
function generate( src, dst, opts, logger, errHandler ) {
_log = logger || console.log;
_err = errHandler || error;
//_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
// Load input resumes...
if( !src || !src.length ) { throw { fluenterror: 3 }; }
var sheets = loadSourceResumes( src, _log );
// Merge input resumes...
var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ?
'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName;
return extend( true, b, a );
});
msg && _log(msg);
// Verify the specified theme name/path
var relativeThemeFolder = '../../node_modules/fluent-themes/themes';
var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme);
var exists = require('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;
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
(' theme (' + Object.keys(theTheme.formats).length + ' formats)').info);
// Expand output resumes... (can't use map() here)
var targets = [], that = this;
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) {
var to = PATH.resolve(t),
pa = 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!
var finished = targets.map( function(t) { return single(t, theTheme); });
// Don't send the client back empty-handed
return { sheet: rez, targets: targets, processed: finished };
};
/**
Generate a single resume of a specific format.
@param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function single( targInfo, theme ) {
try {
var f = targInfo.file
, fType = targInfo.fmt.outFormat
, fName = PATH.basename(f, '.' + fType)
, theFormat;
// If targInfo.fmt.files exists, this theme has an explicit "files"
// section in its theme.json file.
if( targInfo.fmt.files && targInfo.fmt.files.length ) {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
// targInfo.fmt.files.forEach( function( form ) {
//
// if( form.action === 'transform' ) {
// var theFormat = _fmts.filter( function( fmt ) {
// return fmt.name === targInfo.fmt.outFormat;
// })[0];
// MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
// theFormat.gen.generate( rez, f, _opts );
// }
// else if( form.action === null ) {
// // Copy the file
// }
//
// });
}
// Otherwise the theme has no files section
else {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
}
}
catch( ex ) {
_err( ex );
}
}
}());

97
src/verbs/validate.js Normal file
View File

@ -0,0 +1,97 @@
(function() {
var FS = require('fs');
var loadSourceResumes = require('../core/load-source-resumes');
module.exports =
/**
Validate 1 to N resumes in either FRESH or JSON Resume format.
*/
function validate( src, unused, opts, logger ) {
var _log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
var isValid = true;
var validator = require('is-my-json-valid');
var schemas = {
fresh: require('fresca'),
jars: require('../core/resume.json')
};
// Load input resumes...
var sheets = loadSourceResumes(src, _log, function( res ) {
try {
return {
file: res,
raw: FS.readFileSync( res, 'utf8' )
};
}
catch( ex ) {
throw ex;
}
});
sheets.forEach( function( rep ) {
var rez;
try {
rez = JSON.parse( rep.raw );
}
catch( ex ) { // Note [1]
_log('Validating '.info + rep.file.infoBold +
' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold);
if (ex instanceof SyntaxError) {
// Invalid JSON
_log( '--> '.bold.red + rep.file.toUpperCase().red +
' contains invalid JSON. Unable to validate.'.red );
_log( (' INTERNAL: ' + ex).red );
}
else {
_log(('ERROR: ' + ex.toString()).red.bold);
}
return;
}
var isValid = false;
var style = 'useful';
var errors = [];
var fmt = rez.meta &&
(rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars';
try {
var validate = validator( schemas[ fmt ], { // Note [1]
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
isValid = validate( rez );
if( !isValid ) {
style = 'warn';
errors = validate.errors;
}
}
catch(ex) {
return;
}
_log( 'Validating '.info + rep.file.infoBold + ' against '.info +
fmt.replace('jars','JSON Resume').toUpperCase().infoBold +
' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold );
errors.forEach(function(err,idx) {
_log( '--> '.bold.yellow +
(err.field.replace('data.','resume.').toUpperCase() + ' ' +
err.message).yellow );
});
});
};
}());

View File

@ -0,0 +1,91 @@
{
"basics": {
"name": "",
"label": "",
"picture": "",
"email": "",
"phone": "",
"degree": "",
"website": "",
"summary": "",
"location": {
"address": "",
"postalCode": "",
"city": "",
"countryCode": "",
"region": ""
},
"profiles": [
{
"network": "",
"username": "",
"url": ""
}
]
},
"work": [
{
"company": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [
""
]
}
],
"awards": [
{
"title": "",
"date": "",
"awarder": "",
"summary": ""
}
],
"education": [
{
"institution": "",
"area": "",
"studyType": "",
"startDate": "",
"endDate": "",
"gpa": "",
"courses": [
""
]
}
],
"publications": [
{
"name": "",
"publisher": "",
"releaseDate": "",
"website": "",
"summary": ""
}
],
"volunteer": [
{
"organization": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [
""
]
}
],
"skills": [
{
"name": "",
"level": "",
"keywords": [
""
]
}
]
}

View File

@ -0,0 +1,104 @@
{
"basics": {
"name": "Jane Q. Fullstacker"
},
"education": [
{
"gpa": "3.5",
"courses": [
"Course 1",
"Course 2",
"Course 2"
],
"startDate": "2005-09",
"endDate": "2008-05"
},
{
"institution": "Medfield College",
"gpa": "3.2",
"courses": [
"Course 1",
"Course 2",
"Course 2"
],
"startDate": "2003-09",
"endDate": "2005-06"
}
],
"skills": [
{
"name": "Web Dev",
"keywords": [
"JavaScript",
"HTML 5",
"CSS",
"LAMP",
"MVC",
"REST"
]
},
{
"name": "JavaScript"
}
],
"volunteer": [],
"publications": [
{
"name": "Building User Interfaces with Electron and Atom",
"releaseDate": "2011",
"website": "http://codeproject.com/build-ui-electron-atom.aspx"
},
{
"name": "Jane Fullstacker's Blog",
"publisher": "self",
"releaseDate": "2011",
"website": "http://janef.me"
},
{
"name": "Teach Yourself GORFF in 21 Days",
"publisher": "Amazon"
}
],
"interests": [
{
"name": "reading",
"summary": "Jane is a fan of mystery novels and courtroom dramas including Agatha Christie and John Grisham.",
"keywords": [
"mystery",
"Agatha Christie",
"John Grisham"
]
},
{
"name": "hiking",
"summary": "Jane enjoys hiking, light mountain climbing, and has four summits under her belt!"
},
{
"name": "yoga"
}
],
"references": [
{
"name": "John Davidson",
"reference": "Jane is awesome! I'd hire her again in a heartbeat."
},
{
"name": "Elias Fullstacker",
"reference": "I worked with Jane on Jabberwocky and can vouch for her awesome technical capabilities and attention to detail. Insta-hire."
},
{
"name": "Dana Nevins",
"reference": "I've known Jane personally and professionally for almost ten years. She is one in a million."
}
],
"languages": [
{
"language": "English",
"level": "Native"
},
{
"language": "Spanish",
"level": "Moderate"
}
]
}

76
tests/test-cli.js Normal file
View File

@ -0,0 +1,76 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, FCMD = require( '../src/hackmycmd')
, validator = require('is-my-json-valid')
, COLORS = require('colors');
chai.config.includeStack = false;
describe('Testing CLI interface', function () {
var _sheet;
function logMsg() {
}
COLORS.setTheme({
title: ['white','bold'],
info: process.platform === 'win32' ? 'gray' : ['white','dim'],
infoBold: ['white','dim'],
warn: 'yellow',
error: 'red',
guide: 'yellow',
status: 'gray',//['white','dim'],
useful: 'green',
});
var opts = {
//theme: 'compact',
format: 'FRESH',
prettify: true,
silent: true
};
var opts2 = {
format: 'JRS',
prettify: true,
silent: true
};
run( 'new', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
run( 'new', ['tests/sandbox/new-1.json', 'tests/sandbox/new-2.json', 'tests/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' );
run( 'new', ['tests/sandbox/new-jrs-1.json', 'tests/sandbox/new-jrs-2.json', 'tests/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' );
run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
fail( 'new', [], [], opts, " (when a filename isn't specified)" );
run( 'validate', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], [], opts, ' (FRESH format)' );
run( 'validate', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
function run( verb, src, dst, opts, msg ) {
msg = msg || '.';
it( 'The ' + verb.toUpperCase() + ' command should SUCCEED' + msg, function () {
function runIt() {
FCMD.verbs[verb]( src, dst, opts, opts.silent ? logMsg : null );
}
runIt.should.not.Throw();
});
}
function fail( verb, src, dst, opts, msg ) {
msg = msg || '.';
it( 'The ' + verb.toUpperCase() + ' command should FAIL' + msg, function () {
function runIt() {
FCMD.verbs[verb]( src, dst, opts, logMsg );
}
runIt.should.Throw();
});
}
});

View File

@ -18,7 +18,7 @@ describe('FRESH/JRS converter', function () {
it('should round-trip from JRS to FRESH to JRS without modifying or losing data', function () {
var fileA = path.join( __dirname, 'resumes/jrs/richard-hendriks.json' );
var fileA = path.join( __dirname, 'resumes/jrs-0.0.0/richard-hendriks.json' );
var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' );
_sheet = new FRESHResume().open( fileA );

View File

@ -55,7 +55,7 @@ describe('jane-doe.json (FRESH)', function () {
it('should validate against the FRESH resume schema', function() {
var result = _sheet.isValid();
// var schemaJson = require('FRESCA');
// 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. " +

View File

@ -9,55 +9,61 @@ var chai = require('chai')
chai.config.includeStack = false;
describe('jane-doe.json (JRS)', function () {
function testResume( opts ) {
describe( opts.title + ' (JRS)', function() {
opts.isValid = opts.isValid !== false;
var _sheet;
it('should open without throwing an exception', function () {
it('should open without throwing an exception', function () {
var that = this;
function tryOpen() {
_sheet = new JRSResume().open(
path.join( __dirname, 'resumes/jrs/jane-q-fullstacker.json' ) );
path.join( __dirname, 'resumes/jrs-0.0.0/' + opts.title + '.json' ) );
}
tryOpen.should.not.Throw();
});
it('should have one or more of each section', function() {
expect(
(_sheet.basics) &&
(_sheet.work && _sheet.work.length > 0) &&
(_sheet.skills && _sheet.skills.length > 0) &&
(_sheet.education && _sheet.education.length > 0) &&
(_sheet.volunteer && _sheet.volunteer.length > 0) &&
(_sheet.publications && _sheet.publications.length > 0) &&
(_sheet.awards && _sheet.awards.length > 0)
).to.equal( true );
var newObj = _.pick( _sheet, opts.sections );
expect( Object.keys(newObj).length ).to.equal( opts.sections.length );
});
it('should have a work duration of 7 years', function() {
_sheet.basics.computed.numYears.should.equal( 7 );
it('should have a work duration of ' + opts.duration + ' years', function() {
_sheet.basics.computed.numYears.should.equal( opts.duration );
});
it('should save without throwing an exception', function(){
it('should save without throwing an exception', function() {
var that = this;
function trySave() {
_sheet.save( 'tests/sandbox/jane-q-fullstacker.json' );
_sheet.save( 'tests/sandbox/' + opts.title + '.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new JRSResume().open( 'tests/sandbox/jane-q-fullstacker.json' );
var savedSheet = new JRSResume().open( 'tests/sandbox/' + opts.title + '.json' );
_sheet.stringify().should.equal( savedSheet.stringify() )
});
it('should validate against the JSON Resume schema', function() {
it('should ' + (opts.isValid ? '' : 'NOT ') + 'validate against the JSON Resume schema', function() {
var result = _sheet.isValid();
// var schemaJson = require('../src/core/resume.json');
// 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.basics.imp.validationErrors, "\n\n");
result.should.equal( true );
result.should.equal( opts.isValid );
});
});
}
});
var sects = [ 'basics', 'work', 'volunteer', 'skills', 'education', 'publications', 'awards', 'references' ];
testResume({ title: 'jane-q-fullstacker', duration: 7, sections: sects });
testResume({ title: 'jane-incomplete', duration: 0, sections: _.without(sects, 'awards', 'work') });
testResume({ title: 'richard-hendriks', duration: 1, sections: sects });
testResume({ title: 'empty', duration: 0, sections: [], isValid: false });

View File

@ -35,9 +35,9 @@ describe('Testing themes', function () {
theme: themeName,
format: 'FRESH',
prettify: true,
silent: false
silent: true
};
FCMD.verbs.build( src, dst, opts );
FCMD.verbs.build( src, dst, opts, function() { } );
}
tryOpen.should.not.Throw();
});