diff --git a/Gruntfile.js b/Gruntfile.js index 24e463b..01d3a9c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -10,16 +10,22 @@ module.exports = function (grunt) { main: { expand: true, cwd: 'src', - src: ['**/*','!**/*.coffee'], + src: ['**/*','!**/*.coffee','!hmc/**'], dest: 'dist/', }, + core: { + expand: true, + cwd: 'src', + src: ['hmc/dist/**/*','hmc/package.json'], + dest: 'dist/', + } }, coffee: { main: { expand: true, cwd: 'src', - src: ['**/*.coffee'], + src: ['cli/**/*.coffee'], dest: 'dist/', ext: '.js' } @@ -69,7 +75,7 @@ module.exports = function (grunt) { laxcomma: true, expr: true }, - all: ['Gruntfile.js', 'src/**/*.js', 'test/*.js'] + all: ['Gruntfile.js', 'dist/cli/**/*.js', 'test/*.js'] } }; diff --git a/dist/cli/error.js b/dist/cli/error.js index 1841b35..2d6ca97 100644 --- a/dist/cli/error.js +++ b/dist/cli/error.js @@ -8,19 +8,19 @@ Error-handling routines for HackMyResume. (function() { var ErrorHandler, FCMD, FS, HMSTATUS, M2C, PATH, PKG, SyntaxErrorEx, WRAP, YAML, _defaultLog, assembleError, chalk, extend, printf; - HMSTATUS = require('hackmycore/dist/core/status-codes'); + HMSTATUS = require('../hmc/dist/core/status-codes'); PKG = require('../../package.json'); FS = require('fs'); - FCMD = require('hackmycore'); + FCMD = require('../hmc'); PATH = require('path'); WRAP = require('word-wrap'); - M2C = require('hackmycore/dist/utils/md2chalk.js'); + M2C = require('../hmc/dist/utils/md2chalk.js'); chalk = require('chalk'); @@ -30,7 +30,7 @@ Error-handling routines for HackMyResume. printf = require('printf'); - SyntaxErrorEx = require('hackmycore/dist/utils/syntax-error-ex'); + SyntaxErrorEx = require('../hmc/dist/utils/syntax-error-ex'); require('string.prototype.startswith'); diff --git a/dist/cli/main.js b/dist/cli/main.js index 1ab08bb..a3347fb 100644 --- a/dist/cli/main.js +++ b/dist/cli/main.js @@ -8,7 +8,7 @@ Definition of the `main` function. (function() { var Command, EXTEND, FS, HME, HMR, HMSTATUS, OUTPUT, PAD, PATH, PKG, StringUtils, _, _opts, _out, _title, chalk, execute, initOptions, initialize, loadOptions, logMsg, main, safeLoadJSON, splitSrcDest; - HMR = require('hackmycore'); + HMR = require('../hmc'); PKG = require('../../package.json'); @@ -20,13 +20,13 @@ Definition of the `main` function. PATH = require('path'); - HMSTATUS = require('hackmycore/dist/core/status-codes'); + HMSTATUS = require('../hmc/dist/core/status-codes'); - HME = require('hackmycore/dist/core/event-codes'); + HME = require('../hmc/dist/core/event-codes'); - safeLoadJSON = require('hackmycore/dist/utils/safe-json-loader'); + safeLoadJSON = require('../hmc/dist/utils/safe-json-loader'); - StringUtils = require('hackmycore/dist/utils/string.js'); + StringUtils = require('../hmc/dist/utils/string.js'); _ = require('underscore'); diff --git a/dist/cli/out.js b/dist/cli/out.js index 371f995..ccc3093 100644 --- a/dist/cli/out.js +++ b/dist/cli/out.js @@ -10,13 +10,13 @@ Output routines for HackMyResume. chalk = require('chalk'); - HME = require('hackmycore/dist/core/event-codes'); + HME = require('../hmc/dist/core/event-codes'); _ = require('underscore'); - Class = require('hackmycore/dist/utils/class.js'); + Class = require('../hmc/dist/utils/class.js'); - M2C = require('hackmycore/dist/utils/md2chalk.js'); + M2C = require('../hmc/dist/utils/md2chalk.js'); PATH = require('path'); @@ -42,7 +42,7 @@ Output routines for HackMyResume. OutputHandler = module.exports = Class.extend({ init: function(opts) { this.opts = EXTEND(true, this.opts || {}, opts); - return this.msgs = YAML.load(PATH.join(__dirname, 'msg.yml')).events; + this.msgs = YAML.load(PATH.join(__dirname, 'msg.yml')).events; }, log: function(msg) { var finished; @@ -110,7 +110,7 @@ Output routines for HackMyResume. case HME.afterAnalyze: info = evt.info; rawTpl = FS.readFileSync(PATH.join(__dirname, 'analyze.hbs'), 'utf8'); - HANDLEBARS.registerHelper(require('hackmycore/dist/helpers/console-helpers')); + HANDLEBARS.registerHelper(require('../hmc/dist/helpers/console-helpers')); template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false diff --git a/dist/hmc/dist/core/default-formats.js b/dist/hmc/dist/core/default-formats.js new file mode 100644 index 0000000..44ad54b --- /dev/null +++ b/dist/hmc/dist/core/default-formats.js @@ -0,0 +1,60 @@ + +/* +Event code definitions. +@module core/default-formats +@license MIT. See LICENSE.md for details. + */ + + +/** Supported resume formats. */ + +(function() { + module.exports = [ + { + name: 'html', + ext: 'html', + gen: new (require('../generators/html-generator'))() + }, { + name: 'txt', + ext: 'txt', + gen: new (require('../generators/text-generator'))() + }, { + name: 'doc', + ext: 'doc', + fmt: 'xml', + gen: new (require('../generators/word-generator'))() + }, { + name: 'pdf', + ext: 'pdf', + fmt: 'html', + is: false, + gen: new (require('../generators/html-pdf-cli-generator'))() + }, { + name: 'png', + ext: 'png', + fmt: 'html', + is: false, + gen: new (require('../generators/html-png-generator'))() + }, { + name: 'md', + ext: 'md', + fmt: 'txt', + gen: new (require('../generators/markdown-generator'))() + }, { + name: 'json', + ext: 'json', + gen: new (require('../generators/json-generator'))() + }, { + name: 'yml', + ext: 'yml', + fmt: 'yml', + gen: new (require('../generators/json-yaml-generator'))() + }, { + name: 'latex', + ext: 'tex', + fmt: 'latex', + gen: new (require('../generators/latex-generator'))() + } + ]; + +}).call(this); diff --git a/dist/hmc/dist/core/default-options.js b/dist/hmc/dist/core/default-options.js new file mode 100644 index 0000000..edb0975 --- /dev/null +++ b/dist/hmc/dist/core/default-options.js @@ -0,0 +1,18 @@ + +/* +Event code definitions. +@module core/default-options +@license MIT. See LICENSE.md for details. + */ + +(function() { + module.exports = { + theme: 'modern', + prettify: { + indent_size: 2, + unformatted: ['em', 'strong'], + max_char: 80 + } + }; + +}).call(this); diff --git a/dist/hmc/dist/core/empty-jrs.json b/dist/hmc/dist/core/empty-jrs.json new file mode 100644 index 0000000..bedfe00 --- /dev/null +++ b/dist/hmc/dist/core/empty-jrs.json @@ -0,0 +1,77 @@ +{ + "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": [""] + }] +} diff --git a/dist/hmc/dist/core/event-codes.js b/dist/hmc/dist/core/event-codes.js new file mode 100644 index 0000000..2cf736a --- /dev/null +++ b/dist/hmc/dist/core/event-codes.js @@ -0,0 +1,39 @@ + +/* +Event code definitions. +@module core/event-codes +@license MIT. See LICENSE.md for details. + */ + +(function() { + module.exports = { + error: -1, + success: 0, + begin: 1, + end: 2, + beforeRead: 3, + afterRead: 4, + beforeCreate: 5, + afterCreate: 6, + beforeTheme: 7, + afterTheme: 8, + beforeMerge: 9, + afterMerge: 10, + beforeGenerate: 11, + afterGenerate: 12, + beforeAnalyze: 13, + afterAnalyze: 14, + beforeConvert: 15, + afterConvert: 16, + verifyOutputs: 17, + beforeParse: 18, + afterParse: 19, + beforePeek: 20, + afterPeek: 21, + beforeInlineConvert: 22, + afterInlineConvert: 23, + beforeValidate: 24, + afterValidate: 25 + }; + +}).call(this); diff --git a/dist/hmc/dist/core/fluent-date.js b/dist/hmc/dist/core/fluent-date.js new file mode 100644 index 0000000..3b798f9 --- /dev/null +++ b/dist/hmc/dist/core/fluent-date.js @@ -0,0 +1,125 @@ + +/** +The HackMyResume date representation. +@license MIT. See LICENSE.md for details. +@module core/fluent-date + */ + +(function() { + var FluentDate, abbr, moment, months; + + moment = require('moment'); + + + /** + Create a FluentDate from a string or Moment date object. There are a few date + formats to be aware of here. + 1. The words "Present" and "Now", referring to the current date + 2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10") + 3. Year-and-month only ("2015-04") + 4. Year-only "YYYY" ("2015") + 5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008") + 6. Empty dates ("", " ") + 7. Any other date format that Moment.js can parse from + Note: Moment can transparently parse all or most of these, without requiring us + to specify a date format...but for maximum parsing safety and to avoid Moment + deprecation warnings, it's recommended to either a) explicitly specify the date + format or b) use an ISO format. For clarity, we handle these cases explicitly. + @class FluentDate + */ + + FluentDate = (function() { + function FluentDate(dt) { + this.rep = this.fmt(dt); + } + + return FluentDate; + + })(); + + months = {}; + + abbr = {}; + + moment.months().forEach(function(m, idx) { + return months[m.toLowerCase()] = idx + 1; + }); + + moment.monthsShort().forEach(function(m, idx) { + return abbr[m.toLowerCase()] = idx + 1; + }); + + abbr.sept = 9; + + module.exports = FluentDate; + + FluentDate.fmt = function(dt, throws) { + var defTime, month, mt, parts, ref, temp; + throws = (throws === void 0 || throws === null) || throws; + if (typeof dt === 'string' || dt instanceof String) { + dt = dt.toLowerCase().trim(); + if (/^(present|now|current)$/.test(dt)) { + return moment(); + } else if (/^\D+\s+\d{4}$/.test(dt)) { + parts = dt.split(' '); + month = months[parts[0]] || abbr[parts[0]]; + temp = parts[1] + '-' + ((ref = month < 10) != null ? ref : '0' + { + month: month.toString() + }); + return moment(temp, 'YYYY-MM'); + } else if (/^\d{4}-\d{1,2}$/.test(dt)) { + return moment(dt, 'YYYY-MM'); + } 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; + } else { + mt = moment(dt); + if (mt.isValid()) { + return mt; + } + if (throws) { + throw 'Invalid date format encountered.'; + } + return null; + } + } else { + if (!dt) { + return moment(); + } else if (dt.isValid && dt.isValid()) { + return dt; + } + if (throws) { + throw 'Unknown date object encountered.'; + } + return null; + } + }; + +}).call(this); diff --git a/dist/hmc/dist/core/fresh-resume.js b/dist/hmc/dist/core/fresh-resume.js new file mode 100644 index 0000000..1a7713f --- /dev/null +++ b/dist/hmc/dist/core/fresh-resume.js @@ -0,0 +1,525 @@ + +/** +Definition of the FRESHResume class. +@license MIT. See LICENSE.md for details. +@module core/fresh-resume + */ + +(function() { + var CONVERTER, FS, FreshResume, JRSResume, MD, PATH, XML, _, __, _parseDates, extend, moment, validator; + + FS = require('fs'); + + extend = require('extend'); + + validator = require('is-my-json-valid'); + + _ = require('underscore'); + + __ = require('lodash'); + + PATH = require('path'); + + moment = require('moment'); + + XML = require('xml-escape'); + + MD = require('marked'); + + CONVERTER = require('fresh-jrs-converter'); + + JRSResume = require('./jrs-resume'); + + + /** + 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 + */ + + FreshResume = (function() { + function FreshResume() {} + + + /** Initialize the FreshResume from file. */ + + FreshResume.prototype.open = function(file, opts) { + var raw, ret; + raw = FS.readFileSync(file, 'utf8'); + ret = this.parse(raw, opts); + this.imp.file = file; + return ret; + }; + + + /** Initialize the the FreshResume from JSON string data. */ + + FreshResume.prototype.parse = function(stringData, opts) { + return this.parseJSON(JSON.parse(stringData), opts); + }; + + + /** + Initialize the FreshResume from JSON. + Open and parse the specified FRESH resume. Merge the JSON object model onto + this Sheet instance with extend() and convert sheet dates to a safe & + consistent format. Then sort each section by startDate descending. + @param rep {Object} The raw JSON representation. + @param opts {Object} Resume loading and parsing options. + { + date: Perform safe date conversion. + sort: Sort resume items by date. + compute: Prepare computed resume totals. + } + */ + + FreshResume.prototype.parseJSON = function(rep, opts) { + var ignoreList, scrubbed, that, traverse; + that = this; + traverse = require('traverse'); + ignoreList = []; + scrubbed = traverse(rep).map(function(x) { + if (!this.isLeaf && this.node.ignore) { + if (this.node.ignore === true || this.node.ignore === 'true') { + ignoreList.push(this.node); + return this.remove(); + } + } + }); + extend(true, this, scrubbed); + if (!this.imp) { + opts = opts || {}; + if (opts.imp === void 0 || opts.imp) { + this.imp = this.imp || {}; + this.imp.title = (opts.title || this.imp.title) || this.name; + } + (opts.date === void 0 || opts.date) && _parseDates.call(this); + (opts.sort === void 0 || opts.sort) && this.sort(); + (opts.compute === void 0 || opts.compute) && (this.computed = { + numYears: this.duration(), + keywords: this.keywords() + }); + } + return this; + }; + + + /** Save the sheet to disk (for environments that have disk access). */ + + FreshResume.prototype.save = function(filename) { + this.imp.file = filename || this.imp.file; + FS.writeFileSync(this.imp.file, this.stringify(), 'utf8'); + return this; + }; + + + /** + Save the sheet to disk in a specific format, either FRESH or JSON Resume. + */ + + FreshResume.prototype.saveAs = function(filename, format) { + var newRep; + if (format !== 'JRS') { + this.imp.file = filename || this.imp.file; + FS.writeFileSync(this.imp.file, this.stringify(), 'utf8'); + } else { + newRep = CONVERTER.toJRS(this); + FS.writeFileSync(filename, JRSResume.stringify(newRep), 'utf8'); + } + return this; + }; + + + /** + Duplicate this FreshResume instance. + This method first extend()s this object onto an empty, creating a deep copy, + and then passes the result into a new FreshResume instance via .parseJSON. + We do it this way to create a true clone of the object without re-running any + of the associated processing. + */ + + FreshResume.prototype.dupe = function() { + var jso, rnew; + jso = extend(true, {}, this); + rnew = new FreshResume(); + rnew.parseJSON(jso, {}); + return rnew; + }; + + + /** + Convert this object to a JSON string, sanitizing meta-properties along the + way. + */ + + FreshResume.prototype.stringify = function() { + return FreshResume.stringify(this); + }; + + + /** + Create a copy of this resume in which all string fields have been run through + a transformation function (such as a Markdown filter or XML encoder). + TODO: Move this out of FRESHResume. + */ + + FreshResume.prototype.transformStrings = function(filt, transformer) { + var ret, trx; + ret = this.dupe(); + trx = require('../utils/string-transformer'); + return trx(ret, filt, transformer); + }; + + + /** + Create a copy of this resume in which all fields have been interpreted as + Markdown. + */ + + FreshResume.prototype.markdownify = function() { + var MDIN, trx; + MDIN = function(txt) { + return MD(txt || '').replace(/^\s*
|<\/p>\s*$/gi, '');
+ };
+ trx = function(key, val) {
+ if (key === 'summary') {
+ return MD(val);
+ }
+ return MDIN(val);
+ };
+ return this.transformStrings(['skills', 'url', 'start', 'end', 'date'], trx);
+ };
+
+
+ /**
+ Create a copy of this resume in which all fields have been interpreted as
+ Markdown.
+ */
+
+ FreshResume.prototype.xmlify = function() {
+ var trx;
+ trx = function(key, val) {
+ return XML(val);
+ };
+ return this.transformStrings([], trx);
+ };
+
+
+ /** Return the resume format. */
+
+ FreshResume.prototype.format = function() {
+ return 'FRESH';
+ };
+
+
+ /**
+ Return internal metadata. Create if it doesn't exist.
+ */
+
+ FreshResume.prototype.i = function() {
+ return this.imp = this.imp || {};
+ };
+
+
+ /** Return a unique list of all keywords across all skills. */
+
+ FreshResume.prototype.keywords = function() {
+ var flatSkills;
+ flatSkills = [];
+ if (this.skills) {
+ if (this.skills.sets) {
+ flatSkills = this.skills.sets.map(function(sk) {
+ return sk.skills;
+ }).reduce(function(a, b) {
+ return a.concat(b);
+ });
+ } else if (this.skills.list) {
+ flatSkills = flatSkills.concat(this.skills.list.map(function(sk) {
+ return sk.name;
+ }));
+ }
+ flatSkills = _.uniq(flatSkills);
+ }
+ return flatSkills;
+ };
+
+
+ /**
+ Reset the sheet to an empty state. TODO: refactor/review
+ */
+
+ FreshResume.prototype.clear = function(clearMeta) {
+ clearMeta = ((clearMeta === void 0) && true) || clearMeta;
+ if (clearMeta) {
+ delete this.imp;
+ }
+ delete this.computed;
+ delete this.employment;
+ delete this.service;
+ delete this.education;
+ delete this.recognition;
+ delete this.reading;
+ delete this.writing;
+ delete this.interests;
+ delete this.skills;
+ return delete this.social;
+ };
+
+
+ /**
+ Get a safe count of the number of things in a section.
+ */
+
+ FreshResume.prototype.count = function(obj) {
+ if (!obj) {
+ return 0;
+ }
+ if (obj.history) {
+ return obj.history.length;
+ }
+ if (obj.sets) {
+ return obj.sets.length;
+ }
+ return obj.length || 0;
+ };
+
+
+ /** Add work experience to the sheet. */
+
+ FreshResume.prototype.add = function(moniker) {
+ var defSheet, newObject;
+ defSheet = FreshResume["default"]();
+ newObject = defSheet[moniker].history ? $.extend(true, {}, defSheet[moniker].history[0]) : moniker === 'skills' ? $.extend(true, {}, defSheet.skills.sets[0]) : $.extend(true, {}, defSheet[moniker][0]);
+ this[moniker] = this[moniker] || [];
+ if (this[moniker].history) {
+ this[moniker].history.push(newObject);
+ } else if (moniker === 'skills') {
+ this.skills.sets.push(newObject);
+ } else {
+ this[moniker].push(newObject);
+ }
+ return newObject;
+ };
+
+
+ /**
+ Determine if the sheet includes a specific social profile (eg, GitHub).
+ */
+
+ FreshResume.prototype.hasProfile = function(socialNetwork) {
+ socialNetwork = socialNetwork.trim().toLowerCase();
+ return this.social && _.some(this.social, function(p) {
+ return p.network.trim().toLowerCase() === socialNetwork;
+ });
+ };
+
+
+ /** Return the specified network profile. */
+
+ FreshResume.prototype.getProfile = function(socialNetwork) {
+ socialNetwork = socialNetwork.trim().toLowerCase();
+ return this.social && _.find(this.social, function(sn) {
+ return sn.network.trim().toLowerCase() === socialNetwork;
+ });
+ };
+
+
+ /**
+ Return an array of profiles for the specified network, for when the user
+ has multiple eg. GitHub accounts.
+ */
+
+ FreshResume.prototype.getProfiles = function(socialNetwork) {
+ socialNetwork = socialNetwork.trim().toLowerCase();
+ return this.social && _.filter(this.social, function(sn) {
+ return sn.network.trim().toLowerCase() === socialNetwork;
+ });
+ };
+
+
+ /** Determine if the sheet includes a specific skill. */
+
+ FreshResume.prototype.hasSkill = function(skill) {
+ skill = skill.trim().toLowerCase();
+ return this.skills && _.some(this.skills, function(sk) {
+ return sk.keywords && _.some(sk.keywords, function(kw) {
+ return kw.trim().toLowerCase() === skill;
+ });
+ });
+ };
+
+
+ /** Validate the sheet against the FRESH Resume schema. */
+
+ FreshResume.prototype.isValid = function(info) {
+ var ret, schemaObj, validate;
+ schemaObj = require('fresca');
+ validator = require('is-my-json-valid');
+ validate = validator(schemaObj, {
+ formats: {
+ date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
+ }
+ });
+ ret = validate(this);
+ if (!ret) {
+ this.imp = this.imp || {};
+ this.imp.validationErrors = validate.errors;
+ }
+ 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;
+ };
+
+
+ /**
+ Sort dated things on the sheet by start date descending. Assumes that dates
+ on the sheet have been processed with _parseDates().
+ */
+
+ FreshResume.prototype.sort = function() {
+ var byDateDesc, sortSection;
+ byDateDesc = function(a, b) {
+ if (a.safe.start.isBefore(b.safe.start)) {
+ return 1;
+ } else {
+ return (a.safe.start.isAfter(b.safe.start) && -1) || 0;
+ }
+ };
+ sortSection = function(key) {
+ var ar, datedThings;
+ ar = __.get(this, key);
+ if (ar && ar.length) {
+ datedThings = obj.filter(function(o) {
+ return o.start;
+ });
+ return datedThings.sort(byDateDesc);
+ }
+ };
+ sortSection('employment.history');
+ sortSection('education.history');
+ sortSection('service.history');
+ sortSection('projects');
+ return this.writing && this.writing.sort(function(a, b) {
+ if (a.safe.date.isBefore(b.safe.date)) {
+ return 1;
+ } else {
+ return (a.safe.date.isAfter(b.safe.date) && -1) || 0;
+ }
+ });
+ };
+
+ return FreshResume;
+
+ })();
+
+
+ /**
+ Get the default (starter) sheet.
+ */
+
+ FreshResume["default"] = function() {
+ return new FreshResume().parseJSON(require('fresh-resume-starter'));
+ };
+
+
+ /**
+ Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
+ along the way.
+ */
+
+ FreshResume.stringify = function(obj) {
+ var replacer;
+ replacer = function(key, value) {
+ var exKeys;
+ exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'];
+ if (_.some(exKeys, function(val) {
+ return key.trim() === val;
+ })) {
+ return void 0;
+ } else {
+ return value;
+ }
+ };
+ return JSON.stringify(obj, replacer, 2);
+ };
+
+
+ /**
+ Convert human-friendly dates into formal Moment.js dates for all collections.
+ We don't want to lose the raw textual date as entered by the user, so we store
+ the Moment-ified date as a separate property with a prefix of .safe. For ex:
+ job.startDate is the date as entered by the user. job.safeStartDate is the
+ parsed Moment.js date that we actually use in processing.
+ */
+
+ _parseDates = function() {
+ var _fmt, replaceDatesInObject, that;
+ _fmt = require('./fluent-date').fmt;
+ that = this;
+ replaceDatesInObject = function(obj) {
+ if (!obj) {
+ return;
+ }
+ if (Object.prototype.toString.call(obj) === '[object Array]') {
+ return obj.forEach(function(elem) {
+ return replaceDatesInObject(elem);
+ });
+ } else if (typeof obj === 'object') {
+ if (obj._isAMomentObject || obj.safe) {
+ return;
+ }
+ Object.keys(obj).forEach(function(key) {
+ return replaceDatesInObject(obj[key]);
+ });
+ return ['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');
+ }
+ }
+ });
+ }
+ };
+ return Object.keys(this).forEach(function(member) {
+ return replaceDatesInObject(that[member]);
+ });
+ };
+
+
+ /** Export the Sheet function/ctor. */
+
+ module.exports = FreshResume;
+
+}).call(this);
diff --git a/dist/hmc/dist/core/fresh-theme.js b/dist/hmc/dist/core/fresh-theme.js
new file mode 100644
index 0000000..1441c03
--- /dev/null
+++ b/dist/hmc/dist/core/fresh-theme.js
@@ -0,0 +1,279 @@
+
+/**
+Definition of the FRESHTheme class.
+@module core/fresh-theme
+@license MIT. See LICENSE.md for details.
+ */
+
+(function() {
+ var EXTEND, FRESHTheme, FS, HMSTATUS, PATH, READFILES, _, friendlyName, loadExplicit, loadImplicit, loadSafeJson, moment, parsePath, pathExists, validator;
+
+ FS = require('fs');
+
+ validator = require('is-my-json-valid');
+
+ _ = require('underscore');
+
+ PATH = require('path');
+
+ parsePath = require('parse-filepath');
+
+ pathExists = require('path-exists').sync;
+
+ EXTEND = require('extend');
+
+ HMSTATUS = require('./status-codes');
+
+ moment = require('moment');
+
+ loadSafeJson = require('../utils/safe-json-loader');
+
+ READFILES = require('recursive-readdir-sync');
+
+
+ /*
+ The FRESHTheme class is a representation of a FRESH theme
+ asset. See also: JRSTheme.
+ @class FRESHTheme
+ */
+
+ FRESHTheme = (function() {
+ function FRESHTheme() {}
+
+
+ /*
+ Open and parse the specified theme.
+ */
+
+ FRESHTheme.prototype.open = function(themeFolder) {
+ var cached, formatsHash, pathInfo, that, themeFile, themeInfo;
+ this.folder = themeFolder;
+ pathInfo = parsePath(themeFolder);
+ formatsHash = {};
+ themeFile = PATH.join(themeFolder, 'theme.json');
+ themeInfo = loadSafeJson(themeFile);
+ if (themeInfo.ex) {
+ throw {
+ fluenterror: themeInfo.ex.operation === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError,
+ inner: themeInfo.ex.inner
+ };
+ }
+ that = this;
+ EXTEND(true, this, themeInfo.json);
+ if (this.inherits) {
+ cached = {};
+ _.each(this.inherits, function(th, key) {
+ var d, themePath, themesFolder;
+ themesFolder = require.resolve('fresh-themes');
+ d = parsePath(themeFolder).dirname;
+ themePath = PATH.join(d, th);
+ cached[th] = cached[th] || new FRESHTheme().open(themePath);
+ return formatsHash[key] = cached[th].getFormat(key);
+ });
+ }
+ if (!!this.formats) {
+ formatsHash = loadExplicit.call(this, formatsHash);
+ this.explicit = true;
+ } else {
+ formatsHash = loadImplicit.call(this, formatsHash);
+ }
+ this.formats = formatsHash;
+ this.name = parsePath(this.folder).name;
+ return this;
+ };
+
+
+ /* Determine if the theme supports the specified output format. */
+
+ FRESHTheme.prototype.hasFormat = function(fmt) {
+ return _.has(this.formats, fmt);
+ };
+
+
+ /* Determine if the theme supports the specified output format. */
+
+ FRESHTheme.prototype.getFormat = function(fmt) {
+ return this.formats[fmt];
+ };
+
+ return FRESHTheme;
+
+ })();
+
+
+ /* Load the theme implicitly, by scanning the theme folder for files. TODO:
+ Refactor duplicated code with loadExplicit.
+ */
+
+ loadImplicit = function(formatsHash) {
+ var fmts, major, that, tplFolder;
+ that = this;
+ major = false;
+ tplFolder = PATH.join(this.folder, 'src');
+ fmts = READFILES(tplFolder).map(function(absPath) {
+ var idx, isMajor, obj, outFmt, pathInfo, portion, reg, res;
+ pathInfo = parsePath(absPath);
+ outFmt = '';
+ isMajor = false;
+ portion = pathInfo.dirname.replace(tplFolder, '');
+ if (portion && portion.trim()) {
+ if (portion[1] === '_') {
+ return;
+ }
+ reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
+ res = reg.exec(portion);
+ if (res) {
+ if (res[1] !== 'partials') {
+ outFmt = res[1];
+ } else {
+ that.partials = that.partials || [];
+ that.partials.push({
+ name: pathInfo.name,
+ path: absPath
+ });
+ return null;
+ }
+ }
+ }
+ if (!outFmt) {
+ idx = pathInfo.name.lastIndexOf('-');
+ outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx + 1);
+ isMajor = true;
+ }
+ formatsHash[outFmt] = formatsHash[outFmt] || {
+ outFormat: outFmt,
+ files: []
+ };
+ obj = {
+ action: 'transform',
+ path: absPath,
+ major: isMajor,
+ orgPath: PATH.relative(tplFolder, absPath),
+ ext: pathInfo.extname.slice(1),
+ title: friendlyName(outFmt),
+ pre: outFmt,
+ data: FS.readFileSync(absPath, 'utf8'),
+ css: null
+ };
+ formatsHash[outFmt].files.push(obj);
+ return obj;
+ });
+ this.cssFiles = fmts.filter(function(fmt) {
+ return fmt && (fmt.ext === 'css');
+ });
+ this.cssFiles.forEach(function(cssf) {
+ var idx;
+ idx = _.findIndex(fmts, function(fmt) {
+ return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
+ });
+ cssf.major = false;
+ if (idx > -1) {
+ fmts[idx].css = cssf.data;
+ return fmts[idx].cssPath = cssf.path;
+ } else {
+ if (that.inherits) {
+ return that.overrides = {
+ file: cssf.path,
+ data: cssf.data
+ };
+ }
+ }
+ });
+ return formatsHash;
+ };
+
+
+ /*
+ Load the theme explicitly, by following the 'formats' hash
+ in the theme's JSON settings file.
+ */
+
+ loadExplicit = function(formatsHash) {
+ var act, fmts, that, tplFolder;
+ tplFolder = PATH.join(this.folder, 'src');
+ act = null;
+ that = this;
+ fmts = READFILES(tplFolder).map(function(absPath) {
+ var absPathSafe, idx, obj, outFmt, pathInfo, portion, reg, res;
+ act = null;
+ pathInfo = parsePath(absPath);
+ absPathSafe = absPath.trim().toLowerCase();
+ outFmt = _.find(Object.keys(that.formats), function(fmtKey) {
+ var fmtVal;
+ fmtVal = that.formats[fmtKey];
+ return _.some(fmtVal.transform, function(fpath) {
+ var absPathB;
+ absPathB = PATH.join(that.folder, fpath).trim().toLowerCase();
+ return absPathB === absPathSafe;
+ });
+ });
+ if (outFmt) {
+ act = 'transform';
+ }
+ if (!outFmt) {
+ portion = pathInfo.dirname.replace(tplFolder, '');
+ if (portion && portion.trim()) {
+ reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
+ res = reg.exec(portion);
+ res && (outFmt = res[1]);
+ }
+ }
+ if (!outFmt) {
+ idx = pathInfo.name.lastIndexOf('-');
+ outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx + 1);
+ }
+ formatsHash[outFmt] = formatsHash[outFmt] || {
+ outFormat: outFmt,
+ files: [],
+ symLinks: that.formats[outFmt].symLinks
+ };
+ obj = {
+ action: act,
+ orgPath: PATH.relative(that.folder, absPath),
+ path: absPath,
+ ext: pathInfo.extname.slice(1),
+ title: friendlyName(outFmt),
+ pre: outFmt,
+ data: FS.readFileSync(absPath, 'utf8'),
+ css: null
+ };
+ formatsHash[outFmt].files.push(obj);
+ return obj;
+ });
+ this.cssFiles = fmts.filter(function(fmt) {
+ return fmt.ext === 'css';
+ });
+ this.cssFiles.forEach(function(cssf) {
+ var idx;
+ idx = _.findIndex(fmts, function(fmt) {
+ return fmt.pre === cssf.pre && fmt.ext === 'html';
+ });
+ fmts[idx].css = cssf.data;
+ return fmts[idx].cssPath = cssf.path;
+ });
+ fmts = fmts.filter(function(fmt) {
+ return fmt.ext !== 'css';
+ });
+ return formatsHash;
+ };
+
+
+ /*
+ Return a more friendly name for certain formats.
+ TODO: Refactor
+ */
+
+ friendlyName = function(val) {
+ var friendly;
+ val = val.trim().toLowerCase();
+ friendly = {
+ yml: 'yaml',
+ md: 'markdown',
+ txt: 'text'
+ };
+ return friendly[val] || val;
+ };
+
+ module.exports = FRESHTheme;
+
+}).call(this);
diff --git a/dist/hmc/dist/core/jrs-resume.js b/dist/hmc/dist/core/jrs-resume.js
new file mode 100644
index 0000000..c58b9ff
--- /dev/null
+++ b/dist/hmc/dist/core/jrs-resume.js
@@ -0,0 +1,438 @@
+
+/**
+Definition of the JRSResume class.
+@license MIT. See LICENSE.md for details.
+@module core/jrs-resume
+ */
+
+(function() {
+ var CONVERTER, FS, JRSResume, MD, PATH, _, _parseDates, extend, moment, validator;
+
+ FS = require('fs');
+
+ extend = require('extend');
+
+ validator = require('is-my-json-valid');
+
+ _ = require('underscore');
+
+ PATH = require('path');
+
+ MD = require('marked');
+
+ CONVERTER = require('fresh-jrs-converter');
+
+ moment = require('moment');
+
+
+ /**
+ 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
+ */
+
+ JRSResume = (function() {
+ var clear, format;
+
+ function JRSResume() {}
+
+
+ /** Initialize the JSResume from file. */
+
+ JRSResume.prototype.open = function(file, title) {
+ this.basics = {
+ imp: {
+ file: file,
+ raw: FS.readFileSync(file, 'utf8')
+ }
+ };
+ return this.parse(this.basics.imp.raw, title);
+ };
+
+
+ /** Initialize the the JSResume from string. */
+
+ JRSResume.prototype.parse = function(stringData, opts) {
+ var rep;
+ opts = opts || {};
+ rep = JSON.parse(stringData);
+ return this.parseJSON(rep, opts);
+ };
+
+
+ /**
+ Initialize the JRSResume object from JSON.
+ Open and parse the specified JRS resume. Merge the JSON object model onto
+ this Sheet instance with extend() and convert sheet dates to a safe &
+ consistent format. Then sort each section by startDate descending.
+ @param rep {Object} The raw JSON representation.
+ @param opts {Object} Resume loading and parsing options.
+ {
+ date: Perform safe date conversion.
+ sort: Sort resume items by date.
+ compute: Prepare computed resume totals.
+ }
+ */
+
+ JRSResume.prototype.parseJSON = function(rep, opts) {
+ var ignoreList, scrubbed, that, traverse;
+ opts = opts || {};
+ that = this;
+ traverse = require('traverse');
+ ignoreList = [];
+ scrubbed = traverse(rep).map(function(x) {
+ if (!this.isLeaf && this.node.ignore) {
+ if (this.node.ignore === true || this.node.ignore === 'true') {
+ ignoreList.push(this.node);
+ return this.remove();
+ }
+ }
+ });
+ extend(true, this, scrubbed);
+ if (opts.imp === void 0 || opts.imp) {
+ this.basics.imp = this.basics.imp || {};
+ this.basics.imp.title = (opts.title || this.basics.imp.title) || this.basics.name;
+ this.basics.imp.orgFormat = 'JRS';
+ }
+ (opts.date === void 0 || opts.date) && _parseDates.call(this);
+ (opts.sort === void 0 || opts.sort) && this.sort();
+ if (opts.compute === void 0 || opts.compute) {
+ this.basics.computed = {
+ numYears: this.duration(),
+ keywords: this.keywords()
+ };
+ }
+ return this;
+ };
+
+
+ /** Save the sheet to disk (for environments that have disk access). */
+
+ JRSResume.prototype.save = function(filename) {
+ this.basics.imp.file = filename || this.basics.imp.file;
+ FS.writeFileSync(this.basics.imp.file, this.stringify(this), 'utf8');
+ return this;
+ };
+
+
+ /** Save the sheet to disk in a specific format, either FRESH or JRS. */
+
+ JRSResume.prototype.saveAs = function(filename, format) {
+ var newRep, stringRep;
+ if (format === 'JRS') {
+ this.basics.imp.file = filename || this.basics.imp.file;
+ FS.writeFileSync(this.basics.imp.file, this.stringify(), 'utf8');
+ } else {
+ newRep = CONVERTER.toFRESH(this);
+ stringRep = CONVERTER.toSTRING(newRep);
+ FS.writeFileSync(filename, stringRep, 'utf8');
+ }
+ return this;
+ };
+
+
+ /** Return the resume format. */
+
+ format = function() {
+ return 'JRS';
+ };
+
+ JRSResume.prototype.stringify = function() {
+ return JRSResume.stringify(this);
+ };
+
+
+ /** Return a unique list of all keywords across all skills. */
+
+ JRSResume.prototype.keywords = function() {
+ var flatSkills;
+ flatSkills = [];
+ if (this.skills && this.skills.length) {
+ this.skills.forEach(function(s) {
+ return flatSkills = _.union(flatSkills, s.keywords);
+ });
+ }
+ return flatSkills;
+ };
+
+
+ /**
+ Return internal metadata. Create if it doesn't exist.
+ JSON Resume v0.0.0 doesn't allow additional properties at the root level,
+ so tuck this into the .basic sub-object.
+ */
+
+ JRSResume.prototype.i = function() {
+ this.basics = this.basics || {};
+ this.basics.imp = this.basics.imp || {};
+ return this.basics.imp;
+ };
+
+
+ /** Reset the sheet to an empty state. */
+
+ clear = function(clearMeta) {
+ clearMeta = ((clearMeta === void 0) && true) || clearMeta;
+ if (clearMeta) {
+ delete this.imp;
+ }
+ delete this.basics.computed;
+ delete this.work;
+ delete this.volunteer;
+ delete this.education;
+ delete this.awards;
+ delete this.publications;
+ delete this.interests;
+ delete this.skills;
+ return delete this.basics.profiles;
+ };
+
+
+ /** Add work experience to the sheet. */
+
+ JRSResume.prototype.add = function(moniker) {
+ var defSheet, newObject;
+ defSheet = JRSResume["default"]();
+ newObject = $.extend(true, {}, defSheet[moniker][0]);
+ this[moniker] = this[moniker] || [];
+ this[moniker].push(newObject);
+ return newObject;
+ };
+
+
+ /** Determine if the sheet includes a specific social profile (eg, GitHub). */
+
+ JRSResume.prototype.hasProfile = function(socialNetwork) {
+ socialNetwork = socialNetwork.trim().toLowerCase();
+ return this.basics.profiles && _.some(this.basics.profiles, function(p) {
+ return p.network.trim().toLowerCase() === socialNetwork;
+ });
+ };
+
+
+ /** Determine if the sheet includes a specific skill. */
+
+ JRSResume.prototype.hasSkill = function(skill) {
+ skill = skill.trim().toLowerCase();
+ return this.skills && _.some(this.skills, function(sk) {
+ return sk.keywords && _.some(sk.keywords, function(kw) {
+ return kw.trim().toLowerCase() === skill;
+ });
+ });
+ };
+
+
+ /** Validate the sheet against the JSON Resume schema. */
+
+ JRSResume.prototype.isValid = function() {
+ var ret, schema, schemaObj, validate;
+ schema = FS.readFileSync(PATH.join(__dirname, 'resume.json'), 'utf8');
+ schemaObj = JSON.parse(schema);
+ validator = require('is-my-json-valid');
+ validate = validator(schemaObj, {
+ formats: {
+ date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
+ }
+ });
+ ret = validate(this);
+ if (!ret) {
+ this.basics.imp = this.basics.imp || {};
+ this.basics.imp.validationErrors = validate.errors;
+ }
+ 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;
+ };
+
+
+ /**
+ Sort dated things on the sheet by start date descending. Assumes that dates
+ on the sheet have been processed with _parseDates().
+ */
+
+ JRSResume.prototype.sort = function() {
+ var byDateDesc;
+ byDateDesc = function(a, b) {
+ if (a.safeStartDate.isBefore(b.safeStartDate)) {
+ return 1;
+ } else {
+ return (a.safeStartDate.isAfter(b.safeStartDate) && -1) || 0;
+ }
+ };
+ this.work && this.work.sort(byDateDesc);
+ this.education && this.education.sort(byDateDesc);
+ this.volunteer && this.volunteer.sort(byDateDesc);
+ this.awards && this.awards.sort(function(a, b) {
+ if (a.safeDate.isBefore(b.safeDate)) {
+ return 1;
+ } else {
+ return (a.safeDate.isAfter(b.safeDate) && -1) || 0;
+ }
+ });
+ return this.publications && this.publications.sort(function(a, b) {
+ if (a.safeReleaseDate.isBefore(b.safeReleaseDate)) {
+ return 1;
+ } else {
+ return (a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1) || 0;
+ }
+ });
+ };
+
+ JRSResume.prototype.dupe = function() {
+ var rnew;
+ rnew = new JRSResume();
+ rnew.parse(this.stringify(), {});
+ return rnew;
+ };
+
+
+ /**
+ Create a copy of this resume in which all fields have been interpreted as
+ Markdown.
+ */
+
+ JRSResume.prototype.harden = function() {
+ var HD, HDIN, hardenStringsInObject, ret, that;
+ that = this;
+ ret = this.dupe();
+ HD = function(txt) {
+ return '@@@@~' + txt + '~@@@@';
+ };
+ HDIN = function(txt) {
+ return HD(txt);
+ };
+ hardenStringsInObject = function(obj, inline) {
+ if (!obj) {
+ return;
+ }
+ inline = inline === void 0 || inline;
+ if (Object.prototype.toString.call(obj) === '[object Array]') {
+ return obj.forEach(function(elem, idx, ar) {
+ if (typeof elem === 'string' || elem instanceof String) {
+ return ar[idx] = inline ? HDIN(elem) : HD(elem);
+ } else {
+ return hardenStringsInObject(elem);
+ }
+ });
+ } else if (typeof obj === 'object') {
+ return Object.keys(obj).forEach(function(key) {
+ var sub;
+ sub = obj[key];
+ if (typeof sub === 'string' || sub instanceof String) {
+ if (_.contains(['skills', 'url', 'website', 'startDate', 'endDate', 'releaseDate', 'date', 'phone', 'email', 'address', 'postalCode', 'city', 'country', 'region'], key)) {
+ return;
+ }
+ if (key === 'summary') {
+ return obj[key] = HD(obj[key]);
+ } else {
+ return obj[key] = inline ? HDIN(obj[key]) : HD(obj[key]);
+ }
+ } else {
+ return hardenStringsInObject(sub);
+ }
+ });
+ }
+ };
+ Object.keys(ret).forEach(function(member) {
+ return hardenStringsInObject(ret[member]);
+ });
+ return ret;
+ };
+
+ return JRSResume;
+
+ })();
+
+
+ /** Get the default (empty) sheet. */
+
+ JRSResume["default"] = function() {
+ return new JRSResume().open(PATH.join(__dirname, 'empty-jrs.json'), 'Empty');
+ };
+
+
+ /**
+ Convert this object to a JSON string, sanitizing meta-properties along the
+ way. Don't override .toString().
+ */
+
+ JRSResume.stringify = function(obj) {
+ var replacer;
+ replacer = function(key, value) {
+ var temp;
+ temp = _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'], function(val) {
+ return key.trim() === val;
+ });
+ if (temp) {
+ return void 0;
+ } else {
+ return value;
+ }
+ };
+ return JSON.stringify(obj, replacer, 2);
+ };
+
+
+ /**
+ Convert human-friendly dates into formal Moment.js dates for all collections.
+ We don't want to lose the raw textual date as entered by the user, so we store
+ the Moment-ified date as a separate property with a prefix of .safe. For ex:
+ job.startDate is the date as entered by the user. job.safeStartDate is the
+ parsed Moment.js date that we actually use in processing.
+ */
+
+ _parseDates = function() {
+ var _fmt;
+ _fmt = require('./fluent-date').fmt;
+ this.work && this.work.forEach(function(job) {
+ job.safeStartDate = _fmt(job.startDate);
+ return job.safeEndDate = _fmt(job.endDate);
+ });
+ this.education && this.education.forEach(function(edu) {
+ edu.safeStartDate = _fmt(edu.startDate);
+ return edu.safeEndDate = _fmt(edu.endDate);
+ });
+ this.volunteer && this.volunteer.forEach(function(vol) {
+ vol.safeStartDate = _fmt(vol.startDate);
+ return vol.safeEndDate = _fmt(vol.endDate);
+ });
+ this.awards && this.awards.forEach(function(awd) {
+ return awd.safeDate = _fmt(awd.date);
+ });
+ return this.publications && this.publications.forEach(function(pub) {
+ return pub.safeReleaseDate = _fmt(pub.releaseDate);
+ });
+ };
+
+
+ /**
+ Export the JRSResume function/ctor.
+ */
+
+ module.exports = JRSResume;
+
+}).call(this);
diff --git a/dist/hmc/dist/core/jrs-theme.js b/dist/hmc/dist/core/jrs-theme.js
new file mode 100644
index 0000000..9cbb4b2
--- /dev/null
+++ b/dist/hmc/dist/core/jrs-theme.js
@@ -0,0 +1,103 @@
+
+/**
+Definition of the JRSTheme class.
+@module core/jrs-theme
+@license MIT. See LICENSE.MD for details.
+ */
+
+(function() {
+ var JRSTheme, PATH, _, getFormat, parsePath, pathExists;
+
+ _ = require('underscore');
+
+ PATH = require('path');
+
+ parsePath = require('parse-filepath');
+
+ pathExists = require('path-exists').sync;
+
+
+ /**
+ The JRSTheme class is a representation of a JSON Resume theme asset.
+ @class JRSTheme
+ */
+
+ JRSTheme = (function() {
+ function JRSTheme() {}
+
+ return JRSTheme;
+
+ })();
+
+ ({
+
+ /**
+ Open and parse the specified theme.
+ @method open
+ */
+ open: function(thFolder) {
+ var pathInfo, pkgJsonPath, thApi, thPkg;
+ this.folder = thFolder;
+ pathInfo = parsePath(thFolder);
+ pkgJsonPath = PATH.join(thFolder, 'package.json');
+ if (pathExists(pkgJsonPath)) {
+ thApi = require(thFolder);
+ thPkg = require(pkgJsonPath);
+ this.name = thPkg.name;
+ this.render = (thApi && thApi.render) || void 0;
+ this.engine = 'jrs';
+ 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
+ };
+ }
+ return this;
+ },
+
+ /**
+ Determine if the theme supports the output format.
+ @method hasFormat
+ */
+ hasFormat: function(fmt) {
+ return _.has(this.formats, fmt);
+ }
+
+ /**
+ Return the requested output format.
+ @method getFormat
+ */
+ });
+
+ getFormat = function(fmt) {
+ return this.formats[fmt];
+ };
+
+ module.exports = JRSTheme;
+
+}).call(this);
diff --git a/dist/hmc/dist/core/resume-factory.js b/dist/hmc/dist/core/resume-factory.js
new file mode 100644
index 0000000..fbdb313
--- /dev/null
+++ b/dist/hmc/dist/core/resume-factory.js
@@ -0,0 +1,127 @@
+
+/**
+Definition of the ResumeFactory class.
+@license MIT. See LICENSE.md for details.
+@module core/resume-factory
+ */
+
+(function() {
+ var FS, HACKMYSTATUS, HME, ResumeConverter, ResumeFactory, SyntaxErrorEx, _, _parse, chalk;
+
+ FS = require('fs');
+
+ HACKMYSTATUS = require('./status-codes');
+
+ HME = require('./event-codes');
+
+ ResumeConverter = require('fresh-jrs-converter');
+
+ chalk = require('chalk');
+
+ SyntaxErrorEx = require('../utils/syntax-error-ex');
+
+ _ = require('underscore');
+
+ require('string.prototype.startswith');
+
+
+ /**
+ A simple factory class for FRESH and JSON Resumes.
+ @class ResumeFactory
+ */
+
+ ResumeFactory = module.exports = {
+
+ /**
+ Load one or more resumes from disk.
+
+ @param {Object} opts An options object with settings for the factory as well
+ as passthrough settings for FRESHResume or JRSResume. Structure:
+
+ {
+ format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null)
+ objectify: true, // FRESH/JRSResume or raw JSON?
+ inner: { // Passthru options for FRESH/JRSResume
+ sort: false
+ }
+ }
+ */
+ load: function(sources, opts, emitter) {
+ return sources.map(function(src) {
+ return this.loadOne(src, opts, emitter);
+ }, this);
+ },
+
+ /** Load a single resume from disk. */
+ loadOne: function(src, opts, emitter) {
+ var ResumeClass, info, isFRESH, json, objectify, orgFormat, rez, toFormat;
+ toFormat = opts.format;
+ objectify = opts.objectify;
+ toFormat && (toFormat = toFormat.toLowerCase().trim());
+ info = _parse(src, opts, emitter);
+ if (info.fluenterror) {
+ return info;
+ }
+ json = info.json;
+ isFRESH = json.meta && json.meta.format && json.meta.format.startsWith('FRESH@');
+ orgFormat = isFRESH ? 'fresh' : 'jrs';
+ if (toFormat && (orgFormat !== toFormat)) {
+ json = ResumeConverter['to' + toFormat.toUpperCase()](json);
+ }
+ rez = null;
+ if (objectify) {
+ ResumeClass = require('../core/' + (toFormat || orgFormat) + '-resume');
+ rez = new ResumeClass().parseJSON(json, opts.inner);
+ rez.i().file = src;
+ }
+ return {
+ file: src,
+ json: info.json,
+ rez: rez
+ };
+ }
+ };
+
+ _parse = function(fileName, opts, eve) {
+ var ex, orgFormat, rawData, ret;
+ rawData = null;
+ try {
+ eve && eve.stat(HME.beforeRead, {
+ file: fileName
+ });
+ rawData = FS.readFileSync(fileName, 'utf8');
+ eve && eve.stat(HME.afterRead, {
+ file: fileName,
+ data: rawData
+ });
+ eve && eve.stat(HME.beforeParse, {
+ data: rawData
+ });
+ ret = {
+ json: JSON.parse(rawData)
+ };
+ orgFormat = ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@') ? 'fresh' : 'jrs';
+ eve && eve.stat(HME.afterParse, {
+ file: fileName,
+ data: ret.json,
+ fmt: orgFormat
+ });
+ return ret;
+ } catch (_error) {
+ ex = {
+ fluenterror: rawData ? HACKMYSTATUS.parseError : HACKMYSTATUS.readError,
+ inner: _error,
+ raw: rawData,
+ file: fileName,
+ shouldExit: false
+ };
+ opts.quit && (ex.quit = true);
+ eve && eve.err(ex.fluenterror, ex);
+ if (opts["throw"]) {
+ throw ex;
+ }
+ return ex;
+ }
+ };
+
+}).call(this);
diff --git a/dist/hmc/dist/core/resume.json b/dist/hmc/dist/core/resume.json
new file mode 100644
index 0000000..57bca12
--- /dev/null
+++ b/dist/hmc/dist/core/resume.json
@@ -0,0 +1,380 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Resume Schema",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "basics": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string",
+ "description": "e.g. Web Developer"
+ },
+ "picture": {
+ "type": "string",
+ "description": "URL (as per RFC 3986) to a picture in JPEG or PNG format"
+ },
+ "email": {
+ "type": "string",
+ "description": "e.g. thomas@gmail.com",
+ "format": "email"
+ },
+ "phone": {
+ "type": "string",
+ "description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923"
+ },
+ "website": {
+ "type": "string",
+ "description": "URL (as per RFC 3986) to your website, e.g. personal homepage",
+ "format": "uri"
+ },
+ "summary": {
+ "type": "string",
+ "description": "Write a short 2-3 sentence biography about yourself"
+ },
+ "location": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "address": {
+ "type": "string",
+ "description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li."
+ },
+ "postalCode": {
+ "type": "string"
+ },
+ "city": {
+ "type": "string"
+ },
+ "countryCode": {
+ "type": "string",
+ "description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN"
+ },
+ "region": {
+ "type": "string",
+ "description": "The general region where you live. Can be a US state, or a province, for instance."
+ }
+ }
+ },
+ "profiles": {
+ "type": "array",
+ "description": "Specify any number of social networks that you participate in",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "network": {
+ "type": "string",
+ "description": "e.g. Facebook or Twitter"
+ },
+ "username": {
+ "type": "string",
+ "description": "e.g. neutralthoughts"
+ },
+ "url": {
+ "type": "string",
+ "description": "e.g. http://twitter.com/neutralthoughts"
+ }
+ }
+ }
+ }
+ }
+ },
+ "work": {
+ "type": "array",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "company": {
+ "type": "string",
+ "description": "e.g. Facebook"
+ },
+ "position": {
+ "type": "string",
+ "description": "e.g. Software Engineer"
+ },
+ "website": {
+ "type": "string",
+ "description": "e.g. http://facebook.com",
+ "format": "uri"
+ },
+ "startDate": {
+ "type": "string",
+ "description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
+ "format": "date"
+ },
+ "endDate": {
+ "type": "string",
+ "description": "e.g. 2012-06-29",
+ "format": "date"
+ },
+ "summary": {
+ "type": "string",
+ "description": "Give an overview of your responsibilities at the company"
+ },
+ "highlights": {
+ "type": "array",
+ "description": "Specify multiple accomplishments",
+ "additionalItems": false,
+ "items": {
+ "type": "string",
+ "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
+ }
+ }
+ }
+
+ }
+ },
+ "volunteer": {
+ "type": "array",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "organization": {
+ "type": "string",
+ "description": "e.g. Facebook"
+ },
+ "position": {
+ "type": "string",
+ "description": "e.g. Software Engineer"
+ },
+ "website": {
+ "type": "string",
+ "description": "e.g. http://facebook.com",
+ "format": "uri"
+ },
+ "startDate": {
+ "type": "string",
+ "description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
+ "format": "date"
+ },
+ "endDate": {
+ "type": "string",
+ "description": "e.g. 2012-06-29",
+ "format": "date"
+ },
+ "summary": {
+ "type": "string",
+ "description": "Give an overview of your responsibilities at the company"
+ },
+ "highlights": {
+ "type": "array",
+ "description": "Specify multiple accomplishments",
+ "additionalItems": false,
+ "items": {
+ "type": "string",
+ "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
+ }
+ }
+ }
+
+ }
+ },
+ "education": {
+ "type": "array",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "institution": {
+ "type": "string",
+ "description": "e.g. Massachusetts Institute of Technology"
+ },
+ "area": {
+ "type": "string",
+ "description": "e.g. Arts"
+ },
+ "studyType": {
+ "type": "string",
+ "description": "e.g. Bachelor"
+ },
+ "startDate": {
+ "type": "string",
+ "description": "e.g. 2014-06-29",
+ "format": "date"
+ },
+ "endDate": {
+ "type": "string",
+ "description": "e.g. 2012-06-29",
+ "format": "date"
+ },
+ "gpa": {
+ "type": "string",
+ "description": "grade point average, e.g. 3.67/4.0"
+ },
+ "courses": {
+ "type": "array",
+ "description": "List notable courses/subjects",
+ "additionalItems": false,
+ "items": {
+ "type": "string",
+ "description": "e.g. H1302 - Introduction to American history"
+ }
+ }
+ }
+
+
+ }
+ },
+ "awards": {
+ "type": "array",
+ "description": "Specify any awards you have received throughout your professional career",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "e.g. One of the 100 greatest minds of the century"
+ },
+ "date": {
+ "type": "string",
+ "description": "e.g. 1989-06-12",
+ "format": "date"
+ },
+ "awarder": {
+ "type": "string",
+ "description": "e.g. Time Magazine"
+ },
+ "summary": {
+ "type": "string",
+ "description": "e.g. Received for my work with Quantum Physics"
+ }
+ }
+ }
+ },
+ "publications": {
+ "type": "array",
+ "description": "Specify your publications through your career",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. The World Wide Web"
+ },
+ "publisher": {
+ "type": "string",
+ "description": "e.g. IEEE, Computer Magazine"
+ },
+ "releaseDate": {
+ "type": "string",
+ "description": "e.g. 1990-08-01"
+ },
+ "website": {
+ "type": "string",
+ "description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html"
+ },
+ "summary": {
+ "type": "string",
+ "description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML."
+ }
+ }
+ }
+ },
+ "skills": {
+ "type": "array",
+ "description": "List out your professional skill-set",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. Web Development"
+ },
+ "level": {
+ "type": "string",
+ "description": "e.g. Master"
+ },
+ "keywords": {
+ "type": "array",
+ "description": "List some keywords pertaining to this skill",
+ "additionalItems": false,
+ "items": {
+ "type": "string",
+ "description": "e.g. HTML"
+ }
+ }
+ }
+ }
+ },
+ "languages": {
+ "type": "array",
+ "description": "List any other languages you speak",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "language": {
+ "type": "string",
+ "description": "e.g. English, Spanish"
+ },
+ "fluency": {
+ "type": "string",
+ "description": "e.g. Fluent, Beginner"
+ }
+ }
+ }
+ },
+ "interests": {
+ "type": "array",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. Philosophy"
+ },
+ "keywords": {
+ "type": "array",
+ "additionalItems": false,
+ "items": {
+ "type": "string",
+ "description": "e.g. Friedrich Nietzsche"
+ }
+ }
+ }
+
+ }
+ },
+ "references": {
+ "type": "array",
+ "description": "List references you have received",
+ "additionalItems": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. Timothy Cook"
+ },
+ "reference": {
+ "type": "string",
+ "description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing."
+ }
+ }
+
+ }
+ }
+ }
+}
diff --git a/dist/hmc/dist/core/status-codes.js b/dist/hmc/dist/core/status-codes.js
new file mode 100644
index 0000000..c5d83ad
--- /dev/null
+++ b/dist/hmc/dist/core/status-codes.js
@@ -0,0 +1,37 @@
+
+/**
+Status codes for HackMyResume.
+@module core/status-codes
+@license MIT. See LICENSE.MD for details.
+ */
+
+(function() {
+ module.exports = {
+ success: 0,
+ themeNotFound: 1,
+ copyCss: 2,
+ resumeNotFound: 3,
+ missingCommand: 4,
+ invalidCommand: 5,
+ resumeNotFoundAlt: 6,
+ inputOutputParity: 7,
+ createNameMissing: 8,
+ pdfgeneration: 9,
+ missingPackageJSON: 10,
+ invalid: 11,
+ invalidFormat: 12,
+ notOnPath: 13,
+ readError: 14,
+ parseError: 15,
+ fileSaveError: 16,
+ generateError: 17,
+ invalidHelperUse: 18,
+ mixedMerge: 19,
+ invokeTemplate: 20,
+ compileTemplate: 21,
+ themeLoad: 22,
+ invalidParamCount: 23,
+ missingParam: 24
+ };
+
+}).call(this);
diff --git a/dist/hmc/dist/generators/base-generator.js b/dist/hmc/dist/generators/base-generator.js
new file mode 100644
index 0000000..591b972
--- /dev/null
+++ b/dist/hmc/dist/generators/base-generator.js
@@ -0,0 +1,33 @@
+
+/**
+Definition of the BaseGenerator class.
+@module base-generator.js
+@license MIT. See LICENSE.md for details.
+ */
+
+(function() {
+ var BaseGenerator, Class;
+
+ Class = require('../utils/class');
+
+
+ /**
+ The BaseGenerator class is the root of the generator hierarchy. Functionality
+ common to ALL generators lives here.
+ */
+
+ BaseGenerator = module.exports = Class.extend({
+
+ /** Base-class initialize. */
+ init: function(outputFormat) {
+ return this.format = outputFormat;
+ },
+
+ /** Status codes. */
+ codes: require('../core/status-codes'),
+
+ /** Generator options. */
+ opts: {}
+ });
+
+}).call(this);
diff --git a/dist/hmc/dist/generators/html-generator.js b/dist/hmc/dist/generators/html-generator.js
new file mode 100644
index 0000000..e601852
--- /dev/null
+++ b/dist/hmc/dist/generators/html-generator.js
@@ -0,0 +1,42 @@
+
+/**
+Definition of the HTMLGenerator class.
+@license MIT. See LICENSE.md for details.
+@module html-generator.js
+ */
+
+(function() {
+ var FS, HTML, HtmlGenerator, PATH, TemplateGenerator;
+
+ TemplateGenerator = require('./template-generator');
+
+ FS = require('fs-extra');
+
+ HTML = require('html');
+
+ PATH = require('path');
+
+ require('string.prototype.endswith');
+
+ HtmlGenerator = module.exports = TemplateGenerator.extend({
+ init: function() {
+ return this._super('html');
+ },
+
+ /**
+ Copy satellite CSS files to the destination and optionally pretty-print
+ the HTML resume prior to saving.
+ */
+ onBeforeSave: function(info) {
+ if (info.outputFile.endsWith('.css')) {
+ return info.mk;
+ }
+ if (this.opts.prettify) {
+ return HTML.prettyPrint(info.mk, this.opts.prettify);
+ } else {
+ return info.mk;
+ }
+ }
+ });
+
+}).call(this);
diff --git a/dist/hmc/dist/generators/html-pdf-cli-generator.js b/dist/hmc/dist/generators/html-pdf-cli-generator.js
new file mode 100644
index 0000000..317d428
--- /dev/null
+++ b/dist/hmc/dist/generators/html-pdf-cli-generator.js
@@ -0,0 +1,98 @@
+
+/**
+Definition of the HtmlPdfCLIGenerator class.
+@module html-pdf-generator.js
+@license MIT. See LICENSE.md for details.
+ */
+
+(function() {
+ var FS, HTML, HtmlPdfCLIGenerator, PATH, SLASH, SPAWN, TemplateGenerator, engines;
+
+ TemplateGenerator = require('./template-generator');
+
+ FS = require('fs-extra');
+
+ HTML = require('html');
+
+ PATH = require('path');
+
+ SPAWN = require('../utils/safe-spawn');
+
+ SLASH = require('slash');
+
+
+ /**
+ An HTML-driven PDF resume generator for HackMyResume. Talks to Phantom,
+ wkhtmltopdf, and other PDF engines over a CLI (command-line interface).
+ If an engine isn't installed for a particular platform, error out gracefully.
+ */
+
+ HtmlPdfCLIGenerator = module.exports = TemplateGenerator.extend({
+ init: function() {
+ return this._super('pdf', 'html');
+ },
+
+ /** Generate the binary PDF. */
+ onBeforeSave: function(info) {
+ var ex, safe_eng;
+ try {
+ safe_eng = info.opts.pdf || 'wkhtmltopdf';
+ if (safe_eng !== 'none') {
+ engines[safe_eng].call(this, info.mk, info.outputFile);
+ return null;
+ }
+ } catch (_error) {
+ ex = _error;
+ if (ex.inner && ex.inner.code === 'ENOENT') {
+ throw {
+ fluenterror: this.codes.notOnPath,
+ inner: ex.inner,
+ engine: ex.cmd,
+ stack: ex.inner && ex.inner.stack
+ };
+ } else {
+ throw {
+ fluenterror: this.codes.pdfGeneration,
+ inner: ex,
+ stack: ex.stack
+ };
+ }
+ }
+ }
+ });
+
+ engines = {
+
+ /**
+ Generate a PDF from HTML using wkhtmltopdf's CLI interface.
+ Spawns a child process with `wkhtmltopdf