1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2025-05-15 01:57: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 [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: 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 Use it when you need to submit, upload, print, or email resumes in specific
formats. formats.
@ -58,7 +58,7 @@ formats.
hackmyresume BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all 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 ```bash
# hackmyresume NEW <OUTPUTS> [-f <FORMAT>] # hackmyresume NEW <OUTPUTS> [-f <FORMAT>]
@ -67,7 +67,7 @@ formats.
hackmyresume NEW r1.json r2.json -f jrs 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. formats.
Use it to convert between the two formats to take advantage of tools and Use it to convert between the two formats to take advantage of tools and
services. services.
@ -78,7 +78,7 @@ services.
hackmyresume CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json 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. Resume schema. Use it to make sure your resume data is sufficient and complete.
```bash ```bash

View File

@ -1,6 +1,6 @@
{ {
"name": "hackmyresume", "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.", "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": { "repository": {
"type": "git", "type": "git",
@ -24,7 +24,7 @@
"Underscore", "Underscore",
"template" "template"
], ],
"author": "hacksalot <hacksalot@fluentdesk.com> (https://github.com/hacksalot)", "author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
"license": "MIT", "license": "MIT",
"preferGlobal": "true", "preferGlobal": "true",
"bugs": { "bugs": {
@ -37,17 +37,20 @@
"homepage": "https://github.com/hacksalot/HackMyResume", "homepage": "https://github.com/hacksalot/HackMyResume",
"dependencies": { "dependencies": {
"colors": "^1.1.2", "colors": "^1.1.2",
"fluent-themes": "~0.7.0-beta", "fluent-themes": "~0.7.1-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",
"html": "0.0.10", "html": "0.0.10",
"is-my-json-valid": "^2.12.2", "is-my-json-valid": "^2.12.2",
"jst": "0.0.13", "jst": "0.0.13",
"lodash": "^3.10.1",
"marked": "^0.3.5", "marked": "^0.3.5",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"moment": "^2.10.6", "moment": "^2.10.6",
"parse-filepath": "^0.6.3",
"path-exists": "^2.1.0",
"recursive-readdir-sync": "^1.0.6", "recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.2.0", "simple-html-tokenizer": "^0.2.0",
"underscore": "^1.8.3", "underscore": "^1.8.3",

View File

@ -16,6 +16,7 @@ FRESH to JSON Resume conversion routiens.
/** /**
Convert from JSON Resume format to FRESH. Convert from JSON Resume format to FRESH.
@method toFresh @method toFresh
@todo Refactor
*/ */
toFRESH: function( src, foreign ) { toFRESH: function( src, foreign ) {
@ -67,6 +68,7 @@ FRESH to JSON Resume conversion routiens.
Convert from FRESH format to JSON Resume. Convert from FRESH format to JSON Resume.
@param foreign True if non-JSON-Resume properties should be included in @param foreign True if non-JSON-Resume properties should be included in
the result, false if those properties should be excluded. the result, false if those properties should be excluded.
@todo Refactor
*/ */
toJRS: function( src, foreign ) { toJRS: function( src, foreign ) {
@ -112,6 +114,7 @@ FRESH to JSON Resume conversion routiens.
}; };
function meta( direction, obj ) { function meta( direction, obj ) {
if( !obj ) return obj; // preserve null and undefined
if( direction ) { if( direction ) {
obj = obj || { }; obj = obj || { };
obj.format = obj.format || "FRESH@0.1.0"; obj.format = obj.format || "FRESH@0.1.0";
@ -121,6 +124,7 @@ FRESH to JSON Resume conversion routiens.
} }
function employment( obj, direction ) { function employment( obj, direction ) {
if( !obj ) return obj;
if( !direction ) { if( !direction ) {
return obj && obj.history ? return obj && obj.history ?
obj.history.map(function(emp){ obj.history.map(function(emp){
@ -157,6 +161,7 @@ FRESH to JSON Resume conversion routiens.
function education( obj, direction ) { function education( obj, direction ) {
if( !obj ) return obj;
if( direction ) { if( direction ) {
return obj && obj.length ? { return obj && obj.length ? {
history: obj.map(function(edu){ history: obj.map(function(edu){
@ -191,6 +196,7 @@ FRESH to JSON Resume conversion routiens.
} }
function service( obj, direction, foreign ) { function service( obj, direction, foreign ) {
if( !obj ) return obj;
if( direction ) { if( direction ) {
return { return {
history: obj && obj.length ? obj.map(function(vol) { history: obj && obj.length ? obj.map(function(vol) {
@ -225,6 +231,7 @@ FRESH to JSON Resume conversion routiens.
} }
function social( obj, direction ) { function social( obj, direction ) {
if( !obj ) return obj;
if( direction ) { if( direction ) {
return obj.map(function(pro){ return obj.map(function(pro){
return { return {
@ -247,6 +254,7 @@ FRESH to JSON Resume conversion routiens.
} }
function recognition( obj, direction, foreign ) { function recognition( obj, direction, foreign ) {
if( !obj ) return obj;
if( direction ) { if( direction ) {
return obj && obj.length ? obj.map( return obj && obj.length ? obj.map(
function(awd){ function(awd){
@ -275,6 +283,7 @@ FRESH to JSON Resume conversion routiens.
} }
function references( obj, direction ) { function references( obj, direction ) {
if( !obj ) return obj;
if( direction ) { if( direction ) {
return obj && obj.length && obj.map(function(ref){ return obj && obj.length && obj.map(function(ref){
return { return {
@ -296,6 +305,7 @@ FRESH to JSON Resume conversion routiens.
} }
function writing( obj, direction ) { function writing( obj, direction ) {
if( !obj ) return obj;
if( direction ) { if( direction ) {
return obj.map(function( pub ) { return obj.map(function( pub ) {
return { 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') , extend = require('../utils/extend')
, validator = require('is-my-json-valid') , validator = require('is-my-json-valid')
, _ = require('underscore') , _ = require('underscore')
, __ = require('lodash')
, PATH = require('path') , PATH = require('path')
, moment = require('moment') , moment = require('moment')
, MD = require('marked') , MD = require('marked')
@ -187,15 +188,6 @@ Definition of the FRESHResume class.
return flatSkills; 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. Reset the sheet to an empty state.
*/ */
@ -299,7 +291,7 @@ Definition of the FRESHResume class.
Validate the sheet against the FRESH Resume schema. Validate the sheet against the FRESH Resume schema.
*/ */
FreshResume.prototype.isValid = function( info ) { FreshResume.prototype.isValid = function( info ) {
var schemaObj = require('FRESCA'); var schemaObj = require('fresca');
var validator = require('is-my-json-valid'); var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // Note [1] var validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } 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. sheets that have overlapping jobs.
*/ */
FreshResume.prototype.duration = function() { 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 firstJob = _.last( this.employment.history );
var careerStart = firstJob.start ? firstJob.safe.start : ''; var careerStart = firstJob.start ? firstJob.safe.start : '';
if ((typeof careerStart === 'string' || careerStart instanceof String) && if ((typeof careerStart === 'string' || careerStart instanceof String) &&
@ -341,9 +334,9 @@ Definition of the FRESHResume class.
*/ */
FreshResume.prototype.sort = function( ) { FreshResume.prototype.sort = function( ) {
this.employment.history && this.employment.history.sort( byDateDesc ); __.get(this, 'employment.history') && this.employment.history.sort( byDateDesc );
this.education.history && this.education.history.sort( byDateDesc ); __.get(this, 'education.history') && this.education.history.sort( byDateDesc );
this.service.history && this.service.history.sort( byDateDesc ); __.get(this, 'service.history') && this.service.history.sort( byDateDesc );
// this.awards && this.awards.sort( function(a, b) { // this.awards && this.awards.sort( function(a, b) {
// return( a.safeDate.isBefore(b.safeDate) ) ? 1 // return( a.safeDate.isBefore(b.safeDate) ) ? 1

View File

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

View File

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

View File

@ -22,16 +22,6 @@ Definition of the HTMLGenerator class.
the HTML resume prior to saving. the HTML resume prior to saving.
*/ */
onBeforeSave: function( info ) { 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 ? return this.opts.prettify ?
HTML.prettyPrint( info.mk, this.opts.prettify ) : info.mk; HTML.prettyPrint( info.mk, this.opts.prettify ) : info.mk;
} }

View File

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

View File

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

View File

@ -7,279 +7,9 @@ Internal resume generation logic for HackMyResume.
(function() { (function() {
module.exports = function () { module.exports = function () {
var path = require( 'path' ) var unused = require('./utils/string')
, extend = require( './utils/extend' ) , PATH = require('path');
, unused = require('./utils/string')
, FS = require('fs')
, _ = require('underscore')
, FLUENT = require('./hackmyapi')
, PATH = require('path')
, MKDIRP = require('mkdirp')
//, COLORS = require('colors')
, rez, _log, _err;
/**
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. Display help documentation.
@ -289,55 +19,22 @@ Internal resume generation logic for HackMyResume.
.useful.bold ); .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. Internal module interface. Used by FCV Desktop and HMR.
*/ */
return { return {
verbs: { verbs: {
build: generate, generate: require('./verbs/generate'),
validate: validate, build: require('./verbs/generate'),
convert: convert, validate: require('./verbs/validate'),
new: create, convert: require('./verbs/convert'),
create: require('./verbs/create'),
new: require('./verbs/create'),
help: help help: help
}, },
lib: require('./hackmyapi'), lib: require('./hackmyapi'),
options: _opts, options: require('./core/default-options'),
formats: _fmts formats: require('./core/default-formats')
}; };
}(); }();

View File

@ -68,7 +68,7 @@ function main() {
// Massage inputs and outputs // Massage inputs and outputs
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 ) && 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 ]; var parms = [ src, dst, opts, logMsg ];
// Invoke the action // Invoke the action
@ -91,6 +91,7 @@ function getOpts( args ) {
}; };
} }
// TODO: refactor
function handleError( ex ) { function handleError( ex ) {
var msg = '', exitCode; 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; }).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold;
break; break;
//case 4: msg = title + '\n' + ; 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 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 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; exitCode = ex.fluenterror;
@ -122,8 +124,10 @@ function handleError( ex ) {
var idx = msg.indexOf('Error: '); var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); 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( ('ERROR: ' + trimmed.toString()).red.bold );
console.log( ex.stack.gray);
}
else else
console.log( trimmed.toString() ); 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 () { 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' ); var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' );
_sheet = new FRESHResume().open( fileA ); _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() { it('should validate against the FRESH resume schema', function() {
var result = _sheet.isValid(); var result = _sheet.isValid();
// var schemaJson = require('FRESCA'); // var schemaJson = require('fresca');
// var validate = validator( schemaJson, { verbose: true } ); // var validate = validator( schemaJson, { verbose: true } );
// var result = validate( JSON.parse( _sheet.imp.raw ) ); // var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " + result || console.log("\n\nOops, resume didn't validate. " +

View File

@ -9,55 +9,61 @@ var chai = require('chai')
chai.config.includeStack = false; 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; var _sheet;
it('should open without throwing an exception', function () { it('should open without throwing an exception', function () {
var that = this;
function tryOpen() { function tryOpen() {
_sheet = new JRSResume().open( _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(); tryOpen.should.not.Throw();
}); });
it('should have one or more of each section', function() { it('should have one or more of each section', function() {
expect( var newObj = _.pick( _sheet, opts.sections );
(_sheet.basics) && expect( Object.keys(newObj).length ).to.equal( opts.sections.length );
(_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 );
}); });
it('should have a work duration of 7 years', function() { it('should have a work duration of ' + opts.duration + ' years', function() {
_sheet.basics.computed.numYears.should.equal( 7 ); _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() { function trySave() {
_sheet.save( 'tests/sandbox/jane-q-fullstacker.json' ); _sheet.save( 'tests/sandbox/' + opts.title + '.json' );
} }
trySave.should.not.Throw(); trySave.should.not.Throw();
}); });
it('should not be modified after saving', function() { 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() ) _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 result = _sheet.isValid();
// var schemaJson = require('../src/core/resume.json'); // var schemaJson = require('../src/core/resume.json');
// var validate = validator( schemaJson, { verbose: true } ); // var validate = validator( schemaJson, { verbose: true } );
// var result = validate( JSON.parse( _sheet.imp.raw ) ); // var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " + result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", _sheet.basics.imp.validationErrors, "\n\n"); "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, theme: themeName,
format: 'FRESH', format: 'FRESH',
prettify: true, prettify: true,
silent: false silent: true
}; };
FCMD.verbs.build( src, dst, opts ); FCMD.verbs.build( src, dst, opts, function() { } );
} }
tryOpen.should.not.Throw(); tryOpen.should.not.Throw();
}); });