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

Compare commits

...

14 Commits

Author SHA1 Message Date
bd278268f6 Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-31 12:21:44 -05:00
abe31e30e0 Update license year range to 2016 2016-01-31 12:21:29 -05:00
314d8d8763 Introduce build instructions. 2016-01-31 12:17:17 -05:00
ed0792e8f8 Fix YML/JSON/PNG invalid output format warning.
Fixes #97 but we still need to support standalone PNG (ie, a PNG not
generated as part of a .all output target).
2016-01-31 09:41:00 -05:00
90765bf90b Refactor verb invocations to base. 2016-01-31 08:37:12 -05:00
f1ba7765ee Include date tests. 2016-01-30 20:20:32 -05:00
27c7a0264a Improve date handling. 2016-01-30 20:06:04 -05:00
8e806dc04f Improve duration calcs, intro base resume class. 2016-01-30 16:40:22 -05:00
8ec6b5ed6a Bump version to 1.7.4. 2016-01-30 12:08:02 -05:00
4ef4ec5d42 Remove Node. 4.5.
Travis support 4.1 and 5.0 but not 4.5.
2016-01-30 11:49:27 -05:00
2f523b845b Travis: Add Node 4.5. 2016-01-30 11:40:36 -05:00
1c416f39d3 Fix JSON Resume theme breakage.
Fixes #128.
2016-01-30 11:31:39 -05:00
1de0eff7b3 Merge pull request #114 from pra85/patch-1
Update license year range to 2016
2016-01-29 22:32:45 -05:00
f8a39b0908 Update license year range to 2016 2016-01-30 07:41:15 +05:30
32 changed files with 499 additions and 373 deletions

58
BUILDING.md Normal file
View File

@ -0,0 +1,58 @@
Building
========
*See [CONTRIBUTING.md][contrib] for more information on contributing to the
HackMyResume or FluentCV projects.*
HackMyResume is a standard Node.js command line app implemented in a mix of
CoffeeScript and JavaScript. Setting up a build environment is easy:
## Prerequisites ##
1. OS: Linux, OS X, or Windows
2. Install [Node.js][node] and [Grunt][grunt].
## Set up a build environment ###
1. Fork [hacksalot/HackMyResume][hmr] to your GitHub account.
2. Clone your fork locally.
3. From within the top-level HackMyResume folder, run `npm install` to install
project dependencies.
4. Create a new branch, based on the latest HackMyResume `dev` branch, to
contain your work.
5. Run `npm link` in the HackMyResume folder so that the `hackmyresume` command
will reference your local installation (you may need to
`npm uninstall -g hackmyresume` first).
## Making changes
1. HackMyResume sources live in the [`/src`][src] folder. Always make your edits
there, never in the generated `/dist` folder.
2. After making your changes, run `grunt build` to package the HackMyResume
sources to the `/dist` folder. This will transform CoffeeScript files to
JavaScript and perform other build steps as necessary. In the future, a watch
task or guardfile will be added to automate this step.
3. Do local spot testing with `hackmyresume` as normal.
4. When you're ready to submit your changes, run `grunt test` to run the HMR
test suite. Fix any errors that occur.
5. Commit and push your changes.
6. Submit a pull request targeting the HackMyResume `dev` branch.
[node]: https://nodejs.org/en/
[grunt]: http://gruntjs.com/
[hmr]: https://github.com/hacksalot/HackMyResume
[src]: https://github.com/hacksalot/HackMyResume/tree/master/src
[contrib]: https://github.com/hacksalot/HackMyResume/blob/master/CONTRIBUTING.md

View File

@ -4,17 +4,11 @@ Contributing
*Note: HackMyResume is also available as [FluentCV][fcv]. Contributors are
credited in both.*
HackMyResume needs your help! Our contribution workflow is based on [GitHub
Flow][flow] and we respond to all pull requests and issues, usually within 24
hours. HackMyResume has no corporate affiliation and no commercial basis, which
allows the project to maintain a strict user-first policy, rapid development
velocity, and a liberal stance on contributions and exotic functionality in
keeping with the spirit (and name) of the tool.
In short, your code is welcome here.
## How To Contribute
*See [BUILDING.md][building] for instructions on setting up a HackMyResume
development environment.*
1. Optional: [**open an issue**][iss] identifying the feature or bug you'd like
to implement or fix. This step isn't required — you can start hacking away on
HackMyResume without clearing it with us — but helps avoid duplication of work
@ -25,7 +19,7 @@ similar; call it whatever you like) to perform your work in.
4. **Install dependencies** by running `npm install` in the top-level
HackMyResume folder.
5. Make your **commits** as usual.
6. **Verify** your changes locally with `npm test`.
6. **Verify** your changes locally with `grunt test`.
7. **Push** your commits.
7. **Submit a pull request** from your feature branch to the HackMyResume `dev`
branch.
@ -48,7 +42,7 @@ You can reach hacksalot directly at:
hacksalot@indevious.com
```
Thanks! See you out there in the trenches.
Thanks for your interest in the HackMyResume project.
[fcv]: https://github.com/fluentdesk/fluentcv
[flow]: https://guides.github.com/introduction/flow/
@ -56,3 +50,4 @@ Thanks! See you out there in the trenches.
[ha]: https://github.com/hacksalot
[th]: https://github.com/tomheon
[awesome]: https://github.com/hacksalot/HackMyResume/graphs/contributors
[building]: https://github.com/hacksalot/HackMyResume/blob/master/BUILDING.md

View File

@ -1,7 +1,7 @@
The MIT License
===============
Copyright (c) 2016 hacksalot (https://github.com/hacksalot)
Copyright (c) 2015-2016 hacksalot (https://github.com/hacksalot)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

71
dist/core/abstract-resume.js vendored Normal file
View File

@ -0,0 +1,71 @@
/**
Definition of the AbstractResume class.
@license MIT. See LICENSE.md for details.
@module core/abstract-resume
*/
(function() {
var AbstractResume, FluentDate, _, __;
_ = require('underscore');
__ = require('lodash');
FluentDate = require('./fluent-date');
AbstractResume = (function() {
function AbstractResume() {}
/**
Compute the total duration of the work history.
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
AbstractResume.prototype.duration = function(collKey, startKey, endKey, unit) {
var firstDate, hist, lastDate, new_e;
unit = unit || 'years';
hist = __.get(this, collKey);
if (!hist || !hist.length) {
return 0;
}
new_e = hist.map(function(job) {
var obj;
obj = _.pick(job, [startKey, endKey]);
if (!_.has(obj, endKey)) {
obj[endKey] = 'current';
}
if (obj && (obj[startKey] || obj[endKey])) {
obj = _.pairs(obj);
obj[0][1] = FluentDate.fmt(obj[0][1]);
if (obj.length > 1) {
obj[1][1] = FluentDate.fmt(obj[1][1]);
}
}
return obj;
});
new_e = _.filter(_.flatten(new_e, true), function(v) {
return v && v.length && v[0] && v[0].length;
});
if (!new_e || !new_e.length) {
return 0;
}
new_e = _.sortBy(new_e, function(elem) {
return elem[1].unix();
});
firstDate = _.first(new_e)[1];
lastDate = _.last(new_e)[1];
return lastDate.diff(firstDate, unit);
};
return AbstractResume;
})();
module.exports = AbstractResume;
}).call(this);

View File

@ -10,6 +10,8 @@ The HackMyResume date representation.
moment = require('moment');
require('../utils/string');
/**
Create a FluentDate from a string or Moment date object. There are a few date
@ -33,6 +35,10 @@ The HackMyResume date representation.
this.rep = this.fmt(dt);
}
FluentDate.isCurrent = function(dt) {
return !dt || (String.is(dt) && /^(present|now|current)$/.test(dt));
};
return FluentDate;
})();
@ -54,7 +60,7 @@ The HackMyResume date representation.
module.exports = FluentDate;
FluentDate.fmt = function(dt, throws) {
var defTime, month, mt, parts, ref, temp;
var month, mt, parts, ref, temp;
throws = (throws === void 0 || throws === null) || throws;
if (typeof dt === 'string' || dt instanceof String) {
dt = dt.toLowerCase().trim();
@ -72,33 +78,7 @@ The HackMyResume date representation.
} else if (/^\s*\d{4}\s*$/.test(dt)) {
return moment(dt, 'YYYY');
} else if (/^\s*$/.test(dt)) {
defTime = {
isNull: true,
isBefore: function(other) {
if (other && !other.isNull) {
return true;
} else {
return false;
}
},
isAfter: function(other) {
if (other && !other.isNull) {
return false;
} else {
return false;
}
},
unix: function() {
return 0;
},
format: function() {
return '';
},
diff: function() {
return 0;
}
};
return defTime;
return moment();
} else {
mt = moment(dt);
if (mt.isValid()) {

View File

@ -6,7 +6,9 @@ Definition of the FRESHResume class.
*/
(function() {
var CONVERTER, FS, FreshResume, JRSResume, MD, PATH, XML, _, __, _parseDates, extend, moment, validator;
var AbstractResume, CONVERTER, FS, FluentDate, FreshResume, JRSResume, MD, PATH, XML, _, __, _parseDates, extend, moment, validator,
extend1 = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
FS = require('fs');
@ -30,6 +32,10 @@ Definition of the FRESHResume class.
JRSResume = require('./jrs-resume');
FluentDate = require('./fluent-date');
AbstractResume = require('./abstract-resume');
/**
A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
@ -37,8 +43,12 @@ Definition of the FRESHResume class.
@constructor
*/
FreshResume = (function() {
function FreshResume() {}
FreshResume = (function(superClass) {
extend1(FreshResume, superClass);
function FreshResume() {
return FreshResume.__super__.constructor.apply(this, arguments);
}
/** Initialize the FreshResume from file. */
@ -376,36 +386,8 @@ Definition of the FRESHResume class.
return ret;
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
FreshResume.prototype.duration = function(unit) {
var careerLast, careerStart, empHist, firstJob;
unit = unit || 'years';
empHist = __.get(this, 'employment.history');
if (empHist && empHist.length) {
firstJob = _.last(empHist);
careerStart = firstJob.start ? firstJob.safe.start : '';
if ((typeof careerStart === 'string' || careerStart instanceof String) && !careerStart.trim()) {
return 0;
}
careerLast = _.max(empHist, function(w) {
if (w.safe && w.safe.end) {
return w.safe.end.unix();
} else {
return moment().unix();
}
});
return careerLast.safe.end.diff(careerStart, unit);
}
return 0;
return FreshResume.__super__.duration.call(this, 'employment.history', 'start', 'end', unit);
};
@ -420,7 +402,11 @@ Definition of the FRESHResume class.
if (a.safe.start.isBefore(b.safe.start)) {
return 1;
} else {
return (a.safe.start.isAfter(b.safe.start) && -1) || 0;
if (a.safe.start.isAfter(b.safe.start)) {
return -1;
} else {
return 0;
}
}
};
sortSection = function(key) {
@ -448,7 +434,7 @@ Definition of the FRESHResume class.
return FreshResume;
})();
})(AbstractResume);
/**
@ -499,7 +485,7 @@ Definition of the FRESHResume class.
return;
}
if (Object.prototype.toString.call(obj) === '[object Array]') {
return obj.forEach(function(elem) {
obj.forEach(function(elem) {
return replaceDatesInObject(elem);
});
} else if (typeof obj === 'object') {
@ -509,19 +495,19 @@ Definition of the FRESHResume class.
Object.keys(obj).forEach(function(key) {
return replaceDatesInObject(obj[key]);
});
return ['start', 'end', 'date'].forEach(function(val) {
['start', 'end', 'date'].forEach(function(val) {
if ((obj[val] !== void 0) && (!obj.safe || !obj.safe[val])) {
obj.safe = obj.safe || {};
obj.safe[val] = _fmt(obj[val]);
if (obj[val] && (val === 'start') && !obj.end) {
return obj.safe.end = _fmt('current');
obj.safe.end = _fmt('current');
}
}
});
}
};
return Object.keys(this).forEach(function(member) {
return replaceDatesInObject(that[member]);
Object.keys(this).forEach(function(member) {
replaceDatesInObject(that[member]);
});
};

View File

@ -6,7 +6,9 @@ Definition of the JRSResume class.
*/
(function() {
var CONVERTER, FS, JRSResume, MD, PATH, _, _parseDates, extend, moment, validator;
var AbstractResume, CONVERTER, FS, JRSResume, MD, PATH, _, _parseDates, extend, moment, validator,
extend1 = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
FS = require('fs');
@ -24,6 +26,8 @@ Definition of the JRSResume class.
moment = require('moment');
AbstractResume = require('./abstract-resume');
/**
A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
@ -31,10 +35,14 @@ Definition of the JRSResume class.
@class JRSResume
*/
JRSResume = (function() {
JRSResume = (function(superClass) {
var clear, format;
function JRSResume() {}
extend1(JRSResume, superClass);
function JRSResume() {
return JRSResume.__super__.constructor.apply(this, arguments);
}
/** Initialize the JSResume from file. */
@ -249,30 +257,8 @@ Definition of the JRSResume class.
return ret;
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
JRSResume.prototype.duration = function(unit) {
var careerLast, careerStart;
unit = unit || 'years';
if (this.work && this.work.length) {
careerStart = this.work[this.work.length - 1].safeStartDate;
if ((typeof careerStart === 'string' || careerStart instanceof String) && !careerStart.trim()) {
return 0;
}
careerLast = _.max(this.work, function(w) {
return w.safeEndDate.unix();
}).safeEndDate;
return careerLast.diff(careerStart, unit);
}
return 0;
return JRSResume.__super__.duration.call(this, 'work', 'startDate', 'endDate', unit);
};
@ -372,7 +358,7 @@ Definition of the JRSResume class.
return JRSResume;
})();
})(AbstractResume);
/** Get the default (empty) sheet. */

View File

@ -6,7 +6,7 @@ Definition of the JRSTheme class.
*/
(function() {
var JRSTheme, PATH, _, getFormat, parsePath, pathExists;
var JRSTheme, PATH, _, parsePath, pathExists;
_ = require('underscore');
@ -25,17 +25,13 @@ Definition of the JRSTheme class.
JRSTheme = (function() {
function JRSTheme() {}
return JRSTheme;
})();
({
/**
Open and parse the specified theme.
@method open
*/
open: function(thFolder) {
JRSTheme.prototype.open = function(thFolder) {
var pathInfo, pkgJsonPath, thApi, thPkg;
this.folder = thFolder;
pathInfo = parsePath(thFolder);
@ -78,25 +74,31 @@ Definition of the JRSTheme class.
};
}
return this;
},
};
/**
Determine if the theme supports the output format.
@method hasFormat
*/
hasFormat: function(fmt) {
JRSTheme.prototype.hasFormat = function(fmt) {
return _.has(this.formats, fmt);
}
};
/**
Return the requested output format.
@method getFormat
*/
});
getFormat = function(fmt) {
return this.formats[fmt];
};
JRSTheme.prototype.getFormat = function(fmt) {
return this.formats[fmt];
};
return JRSTheme;
})();
module.exports = JRSTheme;

View File

@ -36,10 +36,11 @@ Definition of the JRSGenerator class.
generate: function(json, jst, format, cssInfo, opts, theme) {
var org, rezHtml, turnoff;
turnoff = ['log', 'error', 'dir'];
org = turnoff.map(c)(function() {
org = turnoff.map(function(c) {
var ret;
ret = console[c];
return console[c] = function() {};
console[c] = function() {};
return ret;
});
rezHtml = theme.render(json.harden());
turnoff.forEach(function(c, idx) {

View File

@ -26,14 +26,7 @@ Implementation of the 'analyze' verb for HackMyResume.
AnalyzeVerb = module.exports = Verb.extend({
init: function() {
return this._super('analyze');
},
invoke: function() {
this.stat(HMEVENT.begin, {
cmd: 'analyze'
});
analyze.apply(this, arguments);
return this.stat(HMEVENT.end);
return this._super('analyze', analyze);
}
});

41
dist/verbs/build.js vendored
View File

@ -74,18 +74,7 @@ Implementation of the 'build' verb for HackMyResume.
/** Create a new build verb. */
init: function() {
return this._super('build');
},
/** Invoke the Build command. */
invoke: function() {
var ret;
this.stat(HMEVENT.begin, {
cmd: 'build'
});
ret = build.apply(this, arguments);
this.stat(HMEVENT.end);
return ret;
return this._super('build', build);
}
});
@ -95,16 +84,16 @@ Implementation of the 'build' verb for HackMyResume.
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.
@param opts Generation options.
*/
build = function(src, dst, opts) {
var ex, inv, isFRESH, mixed, newEx, orgFormat, rez, sheetObjects, sheets, tFolder, targets, theme, toFormat;
var ex, inv, isFRESH, mixed, newEx, orgFormat, problemSheets, rez, sheetObjects, sheets, tFolder, targets, theme, toFormat;
if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
prep(src, dst, opts);
sheetObjects = ResumeFactory.load(src, {
@ -115,9 +104,12 @@ Implementation of the 'build' verb for HackMyResume.
sort: _opts.sort
}
}, this);
if (!sheetObjects || _.some(sheetObjects, function(so) {
problemSheets = _.filter(sheetObjects, function(so) {
return so.fluenterror;
})) {
});
if (problemSheets && problemSheets.length) {
problemSheets[0].quit = true;
this.err(problemSheets[0].fluenterror, problemSheets[0]);
return null;
}
sheets = sheetObjects.map(function(r) {
@ -130,12 +122,14 @@ Implementation of the 'build' verb for HackMyResume.
try {
tFolder = verifyTheme.call(this, _opts.theme);
theme = _opts.themeObj = loadTheme(tFolder);
addFreebieFormats(theme);
} catch (_error) {
ex = _error;
newEx = {
fluenterror: HMSTATUS.themeLoad,
inner: ex,
attempted: _opts.theme
attempted: _opts.theme,
quit: true
};
this.err(HMSTATUS.themeLoad, newEx);
return null;
@ -147,8 +141,10 @@ Implementation of the 'build' verb for HackMyResume.
if (inv && inv.length) {
this.err(HMSTATUS.invalidFormat, {
data: inv,
theme: theme
theme: theme,
quit: true
});
return null;
}
rez = null;
if (sheets.length > 1) {
@ -186,7 +182,6 @@ Implementation of the 'build' verb for HackMyResume.
fmt: toFormat
});
}
addFreebieFormats(theme);
this.stat(HMEVENT.applyTheme, {
r: rez,
theme: theme
@ -247,12 +242,12 @@ Implementation of the 'build' verb for HackMyResume.
fmt: targInfo.fmt.outFormat,
file: PATH.relative(process.cwd(), f)
});
_opts.targets = finished;
if (targInfo.fmt.files && targInfo.fmt.files.length) {
theFormat = _fmts.filter(function(fmt) {
return fmt.name === targInfo.fmt.outFormat;
})[0];
MKDIRP.sync(PATH.dirname(f));
_opts.targets = finished;
ret = theFormat.gen.generate(_rezObj, f, _opts);
} else {
theFormat = _fmts.filter(function(fmt) {
@ -284,9 +279,7 @@ Implementation of the 'build' verb for HackMyResume.
};
/**
Ensure that user-specified outputs/targets are valid.
*/
/** Ensure that user-specified outputs/targets are valid. */
verifyOutputs = function(targets, theme) {
this.stat(HMEVENT.verifyOutputs, {

View File

@ -22,14 +22,7 @@ Implementation of the 'convert' verb for HackMyResume.
ConvertVerb = module.exports = Verb.extend({
init: function() {
return this._super('convert');
},
invoke: function() {
this.stat(HMEVENT.begin, {
cmd: 'convert'
});
convert.apply(this, arguments);
return this.stat(HMEVENT.end);
return this._super('convert', convert);
}
});

11
dist/verbs/create.js vendored
View File

@ -24,16 +24,7 @@ Implementation of the 'create' verb for HackMyResume.
CreateVerb = module.exports = Verb.extend({
init: function() {
return this._super('new');
},
invoke: function() {
this.stat(HMEVENT.begin, {
cmd: 'create'
});
create.apply(this, arguments);
this.stat(HMEVENT.begin, {
cmd: 'convert'
});
return this._super('new', create);
}
});

9
dist/verbs/peek.js vendored
View File

@ -22,14 +22,7 @@ Implementation of the 'peek' verb for HackMyResume.
PeekVerb = module.exports = Verb.extend({
init: function() {
return this._super('peek');
},
invoke: function() {
this.stat(HMEVENT.begin, {
cmd: 'peek'
});
peek.apply(this, arguments);
return this.stat(HMEVENT.end);
return this._super('peek', peek);
}
});

View File

@ -31,16 +31,7 @@ Implementation of the 'validate' verb for HackMyResume.
ValidateVerb = module.exports = Verb.extend({
init: function() {
return this._super('validate');
},
invoke: function() {
var ret;
this.stat(HMEVENT.begin, {
cmd: 'validate'
});
ret = validate.apply(this, arguments);
this.stat(HMEVENT.end);
return ret;
return this._super('validate', validate);
}
});
@ -104,7 +95,6 @@ Implementation of the 'validate' verb for HackMyResume.
shouldExit: true
};
}
console.log('1111');
return ret;
}, this);
};

18
dist/verbs/verb.js vendored
View File

@ -6,12 +6,14 @@ Definition of the Verb class.
*/
(function() {
var Class, EVENTS, Verb;
var Class, EVENTS, HMEVENT, Verb;
Class = require('../utils/class');
EVENTS = require('events');
HMEVENT = require('../core/event-codes');
/**
An instantiation of a HackMyResume command.
@ -21,9 +23,21 @@ Definition of the Verb class.
Verb = module.exports = Class.extend({
/** Constructor. Automatically called at creation. */
init: function(moniker) {
init: function(moniker, workhorse) {
this.moniker = moniker;
this.emitter = new EVENTS.EventEmitter();
this.workhorse = workhorse;
},
/** Invoke the command. */
invoke: function() {
var ret;
this.stat(HMEVENT.begin, {
cmd: this.moniker
});
ret = this.workhorse.apply(this, arguments);
this.stat(HMEVENT.end);
return ret;
},
/** Forward subscriptions to the event emitter. */

View File

@ -1,6 +1,6 @@
{
"name": "hackmyresume",
"version": "1.7.3",
"version": "1.7.4",
"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",

View File

@ -0,0 +1,53 @@
###*
Definition of the AbstractResume class.
@license MIT. See LICENSE.md for details.
@module core/abstract-resume
###
_ = require 'underscore'
__ = require 'lodash'
FluentDate = require('./fluent-date')
class AbstractResume
###*
Compute the total duration of the work history.
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
###
duration: (collKey, startKey, endKey, unit) ->
unit = unit || 'years'
hist = __.get @, collKey
return 0 if !hist or !hist.length
# BEGIN CODE DUPLICATION --> src/inspectors/gap-inspector.coffee (TODO)
# Convert the candidate's employment history to an array of dates,
# where each element in the array is a start date or an end date of a
# job -- it doesn't matter which.
new_e = hist.map ( job ) ->
obj = _.pick( job, [startKey, endKey] )
# Synthesize an end date if this is a "current" gig
obj[endKey] = 'current' if !_.has obj, endKey
if obj && (obj[startKey] || obj[endKey])
obj = _.pairs obj
obj[0][1] = FluentDate.fmt( obj[0][1] )
if obj.length > 1
obj[1][1] = FluentDate.fmt( obj[1][1] )
obj
# Flatten the array, remove empties, and sort
new_e = _.filter _.flatten( new_e, true ), (v) ->
return v && v.length && v[0] && v[0].length
return 0 if !new_e or !new_e.length
new_e = _.sortBy new_e, ( elem ) -> return elem[1].unix()
# END CODE DUPLICATION
firstDate = _.first( new_e )[1];
lastDate = _.last( new_e )[1];
lastDate.diff firstDate, unit
module.exports = AbstractResume

View File

@ -7,6 +7,7 @@ The HackMyResume date representation.
moment = require 'moment'
require('../utils/string')
###*
Create a FluentDate from a string or Moment date object. There are a few date
@ -30,6 +31,8 @@ class FluentDate
constructor: (dt) ->
@rep = this.fmt dt
@isCurrent: (dt) ->
!dt || (String.is(dt) and /^(present|now|current)$/.test(dt))
months = {}
abbr = {}
@ -56,16 +59,7 @@ FluentDate.fmt = ( dt, throws ) ->
else if /^\s*\d{4}\s*$/.test(dt) # "2015"
return moment dt, 'YYYY'
else if /^\s*$/.test(dt) # "", " "
defTime =
isNull: true
isBefore: ( other ) ->
if other and !other.isNull then true else false
isAfter: ( other ) ->
if other and !other.isNull then false else false
unix: () -> 0
format: () -> ''
diff: () -> 0
return defTime
return moment()
else
mt = moment dt
if mt.isValid()

View File

@ -17,6 +17,8 @@ XML = require 'xml-escape'
MD = require 'marked'
CONVERTER = require 'fresh-jrs-converter'
JRSResume = require './jrs-resume'
FluentDate = require './fluent-date'
AbstractResume = require './abstract-resume'
@ -25,7 +27,7 @@ A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
object is an instantiation of that JSON decorated with utility methods.
@constructor
###
class FreshResume
class FreshResume extends AbstractResume
###* Initialize the FreshResume from file. ###
open: ( file, opts ) ->
@ -306,28 +308,8 @@ class FreshResume
ret
###*
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
###
duration: (unit) ->
unit = unit || 'years'
empHist = __.get(this, 'employment.history')
if empHist && empHist.length
firstJob = _.last( empHist )
careerStart = if firstJob.start then firstJob.safe.start else ''
if ((typeof careerStart == 'string' || careerStart instanceof String) && !careerStart.trim())
return 0
careerLast = _.max empHist, ( w ) ->
return if w.safe && w.safe.end then w.safe.end.unix() else moment().unix()
return careerLast.safe.end.diff careerStart, unit
0
super('employment.history', 'start', 'end', unit)
###*
@ -337,9 +319,9 @@ class FreshResume
sort: () ->
byDateDesc = (a,b) ->
if ( a.safe.start.isBefore(b.safe.start) )
if a.safe.start.isBefore(b.safe.start)
then 1
else ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0
else ( if a.safe.start.isAfter(b.safe.start) then -1 else 0 )
sortSection = ( key ) ->
ar = __.get this, key
@ -352,10 +334,6 @@ class FreshResume
sortSection 'service.history'
sortSection 'projects'
# this.awards && this.awards.sort( function(a, b) {
# return( a.safeDate.isBefore(b.safeDate) ) ? 1
# : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
# });
@writing && @writing.sort (a, b) ->
if a.safe.date.isBefore b.safe.date
then 1
@ -400,6 +378,7 @@ _parseDates = () ->
return if !obj
if Object.prototype.toString.call( obj ) == '[object Array]'
obj.forEach (elem) -> replaceDatesInObject( elem )
return
else if typeof obj == 'object'
if obj._isAMomentObject || obj.safe
return
@ -410,8 +389,12 @@ _parseDates = () ->
obj.safe[ val ] = _fmt obj[val]
if obj[val] && (val == 'start') && !obj.end
obj.safe.end = _fmt 'current'
Object.keys( this ).forEach (member) -> replaceDatesInObject(that[member])
return
return
Object.keys( this ).forEach (member) ->
replaceDatesInObject(that[member])
return
return

View File

@ -14,7 +14,7 @@ PATH = require('path')
MD = require('marked')
CONVERTER = require('fresh-jrs-converter')
moment = require('moment')
AbstractResume = require('./abstract-resume')
###*
@ -22,7 +22,7 @@ A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
is an instantiation of that JSON decorated with utility methods.
@class JRSResume
###
class JRSResume
class JRSResume extends AbstractResume
@ -203,25 +203,8 @@ class JRSResume
ret
###*
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
###
duration: ( unit ) ->
unit = unit || 'years';
if this.work && this.work.length
careerStart = this.work[ this.work.length - 1].safeStartDate
if (typeof careerStart == 'string' || careerStart instanceof String) && !careerStart.trim()
return 0
careerLast = _.max( this.work, ( w ) -> w.safeEndDate.unix() ).safeEndDate;
return careerLast.diff careerStart, unit
0
duration: (unit) ->
super('work', 'startDate', 'endDate', unit)
###*

View File

@ -20,68 +20,69 @@ The JRSTheme class is a representation of a JSON Resume theme asset.
class JRSTheme
###*
Open and parse the specified theme.
@method open
###
open: ( thFolder ) ->
@folder = thFolder
###*
Open and parse the specified theme.
@method open
###
open: ( thFolder ) ->
# Open the [theme-name].json file; should have the same
# name as folder
pathInfo = parsePath thFolder
@folder = thFolder
# Open and parse the theme's package.json file.
pkgJsonPath = PATH.join thFolder, 'package.json'
if pathExists pkgJsonPath
thApi = require thFolder
thPkg = require pkgJsonPath
this.name = thPkg.name
this.render = (thApi && thApi.render) || undefined
this.engine = 'jrs'
# Open the [theme-name].json file; should have the same
# name as folder
pathInfo = parsePath thFolder
# Create theme formats (HTML and PDF). Just add the bare minimum mix of
# properties necessary to allow JSON Resume themes to share a rendering
# path with FRESH themes.
this.formats =
html:
outFormat: 'html'
files: [{
action: 'transform',
render: this.render,
major: true,
ext: 'html',
css: null
}]
pdf:
outFormat: 'pdf'
files: [{
action: 'transform',
render: this.render,
major: true,
ext: 'pdf',
css: null
}]
else
throw { fluenterror: HACKMYSTATUS.missingPackageJSON };
@
# Open and parse the theme's package.json file.
pkgJsonPath = PATH.join thFolder, 'package.json'
if pathExists pkgJsonPath
thApi = require thFolder
thPkg = require pkgJsonPath
this.name = thPkg.name
this.render = (thApi && thApi.render) || undefined
this.engine = 'jrs'
# Create theme formats (HTML and PDF). Just add the bare minimum mix of
# properties necessary to allow JSON Resume themes to share a rendering
# path with FRESH themes.
this.formats =
html:
outFormat: 'html'
files: [{
action: 'transform',
render: this.render,
major: true,
ext: 'html',
css: null
}]
pdf:
outFormat: 'pdf'
files: [{
action: 'transform',
render: this.render,
major: true,
ext: 'pdf',
css: null
}]
else
throw { fluenterror: HACKMYSTATUS.missingPackageJSON };
@
###*
Determine if the theme supports the output format.
@method hasFormat
###
hasFormat: ( fmt ) -> _.has this.formats, fmt
###*
Determine if the theme supports the output format.
@method hasFormat
###
hasFormat: ( fmt ) -> _.has this.formats, fmt
###*
Return the requested output format.
@method getFormat
###
getFormat = ( fmt ) -> @formats[ fmt ]
###*
Return the requested output format.
@method getFormat
###
getFormat: ( fmt ) -> @formats[ fmt ]
module.exports = JRSTheme;

View File

@ -25,9 +25,10 @@ JRSGenerator = module.exports =
# Disable JRS theme chatter (console.log, console.error, etc.)
turnoff = ['log', 'error', 'dir'];
org = turnoff.map(c) ->
org = turnoff.map (c) ->
ret = console[c]
console[c] = () ->
ret
# Freeze and render
rezHtml = theme.render json.harden()

View File

@ -19,12 +19,7 @@ chalk = require('chalk')
AnalyzeVerb = module.exports = Verb.extend
init: -> @._super 'analyze'
invoke: ->
@.stat HMEVENT.begin, { cmd: 'analyze' }
analyze.apply @, arguments
@.stat HMEVENT.end
init: -> @_super 'analyze', analyze

View File

@ -42,14 +42,7 @@ loadTheme = null
BuildVerb = module.exports = Verb.extend
###* Create a new build verb. ###
init: () -> @._super 'build'
###* Invoke the Build command. ###
invoke: ->
@stat HMEVENT.begin, { cmd: 'build' }
ret = build.apply @, arguments
@stat HMEVENT.end
return ret
init: () -> @_super 'build', build
@ -58,13 +51,13 @@ Given a source resume in FRESH or JRS format, a destination resume path, and a
theme file, generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
@param opts Generation options.
###
build = ( src, dst, opts ) ->
if !src || !src.length
@err HMSTATUS.resumeNotFound, { quit: true }
@err HMSTATUS.resumeNotFound, quit: true
return null
prep src, dst, opts
@ -74,10 +67,14 @@ build = ( src, dst, opts ) ->
}, @);
# Explicit check for any resume loading errors...
if !sheetObjects || _.some( sheetObjects, (so) -> return so.fluenterror )
problemSheets = _.filter( sheetObjects, (so) -> return so.fluenterror )
if( problemSheets && problemSheets.length )
problemSheets[0].quit = true
@err problemSheets[0].fluenterror, problemSheets[0]
return null
sheets = sheetObjects.map((r) -> return r.json )
# Get the collection of raw JSON sheets
sheets = sheetObjects.map (r) -> r.json
# Load the theme...
theme = null
@ -85,12 +82,14 @@ build = ( src, dst, opts ) ->
try
tFolder = verifyTheme.call @, _opts.theme
theme = _opts.themeObj = loadTheme tFolder
addFreebieFormats theme
catch ex
newEx =
fluenterror: HMSTATUS.themeLoad
inner: ex
attempted: _opts.theme
this.err HMSTATUS.themeLoad, newEx
quit: true
@err HMSTATUS.themeLoad, newEx
return null
@stat HMEVENT.afterTheme, { theme: theme }
@ -98,7 +97,8 @@ build = ( src, dst, opts ) ->
# Check for invalid outputs...
inv = verifyOutputs.call @, dst, theme
if inv && inv.length
@err HMSTATUS.invalidFormat, { data: inv, theme: theme }
@err HMSTATUS.invalidFormat, data: inv, theme: theme, quit: true
return null
## Merge input resumes, yielding a single source resume.
rez = null
@ -107,7 +107,7 @@ build = ( src, dst, opts ) ->
mixed = _.any sheets, (s) -> return if isFRESH then s.basics else !s.basics
@stat HMEVENT.beforeMerge, { f: _.clone(sheetObjects), mixed: mixed }
if mixed
this.err HMSTATUS.mixedMerge
@err HMSTATUS.mixedMerge
rez = _.reduceRight sheets, ( a, b, idx ) ->
extend( true, b, a )
@ -124,8 +124,7 @@ build = ( src, dst, opts ) ->
rez = RConverter[ 'to' + toFormat ]( rez );
@stat HMEVENT.afterInlineConvert, { file: sheetObjects[0].file, fmt: toFormat }
# Add freebie formats to the theme
addFreebieFormats theme
# Announce the theme
@stat HMEVENT.applyTheme, { r: rez, theme: theme }
# Load the resume into a FRESHResume or JRSResume object
@ -195,13 +194,14 @@ single = ( targInfo, theme, finished ) ->
fmt: targInfo.fmt.outFormat
file: PATH.relative(process.cwd(), f)
_opts.targets = finished;
# If targInfo.fmt.files exists, this format is backed by a document.
# Fluent/FRESH themes are handled here.
if targInfo.fmt.files && targInfo.fmt.files.length
theFormat = _fmts.filter(
(fmt) -> return fmt.name == targInfo.fmt.outFormat )[0];
MKDIRP.sync( PATH.dirname( f ) ); # Ensure dest folder exists;
_opts.targets = finished;
ret = theFormat.gen.generate( _rezObj, f, _opts );
# Otherwise this is an ad-hoc format (JSON, YML, or PNG) that every theme
@ -235,11 +235,9 @@ single = ( targInfo, theme, finished ) ->
###*
Ensure that user-specified outputs/targets are valid.
###
###* Ensure that user-specified outputs/targets are valid. ###
verifyOutputs = ( targets, theme ) ->
@.stat HMEVENT.verifyOutputs, { targets: targets, theme: theme }
@stat HMEVENT.verifyOutputs, { targets: targets, theme: theme }
_.reject targets.map( ( t ) ->
pathInfo = parsePath t
{
@ -247,6 +245,8 @@ verifyOutputs = ( targets, theme ) ->
}),
(t) -> t.format == 'all' || theme.hasFormat( t.format )
###*
Reinforce the chosen theme with "freebie" formats provided by HackMyResume.
A "freebie" format is an output format such as JSON, YML, or PNG that can be

View File

@ -17,12 +17,7 @@ HMEVENT = require('../core/event-codes');
ConvertVerb = module.exports = Verb.extend
init: -> @._super 'convert'
invoke: ->
@.stat HMEVENT.begin, { cmd: 'convert' }
convert.apply @, arguments
@.stat HMEVENT.end
init: -> @_super 'convert', convert

View File

@ -17,13 +17,8 @@ HMEVENT = require('../core/event-codes')
CreateVerb = module.exports = Verb.extend
init: -> @._super('new')
init: -> @_super 'new', create
invoke: ->
@.stat HMEVENT.begin, { cmd: 'create' }
create.apply @, arguments
@.stat HMEVENT.begin, { cmd: 'convert' }
return
###*

View File

@ -17,12 +17,7 @@ HMEVENT = require('../core/event-codes')
PeekVerb = module.exports = Verb.extend
init: -> @._super('peek')
invoke: ->
@.stat HMEVENT.begin, { cmd: 'peek' }
peek.apply @, arguments
@.stat HMEVENT.end
init: -> @_super 'peek', peek

View File

@ -21,13 +21,7 @@ safeLoadJSON = require '../utils/safe-json-loader'
###* An invokable resume validation command. ###
ValidateVerb = module.exports = Verb.extend
init: -> @_super 'validate'
invoke: ->
@stat HMEVENT.begin, { cmd: 'validate' }
ret = validate.apply @, arguments
@stat HMEVENT.end
return ret
init: -> @_super 'validate', validate
@ -85,8 +79,6 @@ validate = (sources, unused, opts) ->
if opts.assert and !ret.isValid
throw fluenterror: HMSTATUS.invalid, shouldExit: true
console.log '1111'
return ret
ret
, @

View File

@ -6,10 +6,10 @@ Definition of the Verb class.
# Use J. Resig's nifty class implementation
Class = require '../utils/class'
EVENTS = require 'events'
HMEVENT = require '../core/event-codes'
@ -22,24 +22,34 @@ Verb = module.exports = Class.extend
###* Constructor. Automatically called at creation. ###
init: ( moniker ) ->
@.moniker = moniker
@.emitter = new EVENTS.EventEmitter()
init: ( moniker, workhorse ) ->
@moniker = moniker
@emitter = new EVENTS.EventEmitter()
@workhorse = workhorse
return
###* Invoke the command. ###
invoke: ->
@stat HMEVENT.begin, cmd: @moniker
ret = @workhorse.apply @, arguments
@stat HMEVENT.end
ret
###* Forward subscriptions to the event emitter. ###
on: ->
this.emitter.on.apply @.emitter, arguments
@emitter.on.apply @emitter, arguments
###* Fire an arbitrary event, scoped to "hmr:". ###
fire: (evtName, payload) ->
payload = payload || { }
payload.cmd = this.moniker
this.emitter.emit 'hmr:' + evtName, payload
payload.cmd = @moniker
@emitter.emit 'hmr:' + evtName, payload
true
@ -60,10 +70,11 @@ Verb = module.exports = Class.extend
stat: ( subEvent, payload ) ->
payload = payload || { }
payload.sub = subEvent
this.fire 'status', payload
@fire 'status', payload
true
###* Associate error info with the invocation. ###
setError: ( code, obj ) ->
@errorCode = code

View File

@ -9,3 +9,4 @@ require('./scripts/test-jrs-sheet');
require('./scripts/test-themes');
require('./scripts/test-api');
require('./scripts/test-output');
require('./scripts/test-dates');

View File

@ -0,0 +1,81 @@
/**
@module test-dates.js
*/
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../../dist/core/fresh-resume')
, FCMD = require( '../../dist/index')
, validator = require('is-my-json-valid')
, EXTEND = require('extend');
chai.config.includeStack = true;
var gig = {
employer: 'E1'
};
var r = {
name: 'John Doe',
meta: {
format: 'FRESH@0.6.0'
},
employment: {
history: [ null ]
}
};
var tests = [
// single job, concrete start, no end
[ { start: '2010-01-01' } , { val: 6, unit: 'year' } ],
[ { start: '2010-01' } , { val: 6, unit: 'year' } ],
[ { start: '2010' } , { val: 6, unit: 'year' } ],
// single job, concrete start, concrete end
[ { start: '2010-01-01', end: '2015-01-01' } , { val: 5, unit: 'year' } ],
[ { start: '2010-01', end: '2015-01' } , { val: 5, unit: 'year' } ],
[ { start: '2010', end: '2015' } , { val: 5, unit: 'year' } ],
// single job, falsy start, falsy end
[ { } , { val: 0, unit: 'year' } ],
[ { start: null } , { val: 0, unit: 'year' } ],
[ { end: null } , { val: 0, unit: 'year' } ],
[ { start: undefined } , { val: 0, unit: 'year' } ],
[ { end: undefined } , { val: 0, unit: 'year' } ],
[ { start: null, end: null } , { val: 0, unit: 'year' } ],
[ { start: '', end: '' } , { val: 0, unit: 'year' } ],
[ { start: ' ', end: ' ' } , { val: 0, unit: 'year' } ],
[ { start: undefined, end: undefined } , { val: 0, unit: 'year' } ],
// two jobs (concrete start + end) -> ( concrete start )
[ { start: '2000-01', end: '2013-01' }, { start: '2013-01' }, { val: 16, unit: 'year' } ],
[ { start: '2000-01', end: '2013-01' }, { start: '2013-01', end: '' }, { val: 16, unit: 'year' } ],
[ { start: '2000-01', end: '2013-01' }, { start: '2013-01', end: null }, { val: 16, unit: 'year' } ],
[ { start: '2000-01', end: '2013-01' }, { start: '2013-01', end: 'current' }, { val: 16, unit: 'year' } ]
];
tests.forEach(function(t){
_.initial( t ).forEach(function(t){ t.employer = 'E1' });
})
describe('Testing DATES', function () {
tests.forEach( function(t) {
it( JSON.stringify( _.initial(t) ), function () {
r.employment.history = _.initial( t );
var rObj = new FRESHResume();
rObj.parseJSON( r );
var dur = rObj.duration();
expect( dur ).to.equal( _.last(t).val );
});
});
});