diff --git a/Gruntfile.js b/Gruntfile.js index 24e463b7..01d3a9c2 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 1841b35c..2d6ca97c 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 1ab08bb8..a3347fba 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 371f9954..ccc30931 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 00000000..44ad54b0 --- /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 00000000..edb09751 --- /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 00000000..bedfe007 --- /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 00000000..2cf736a0 --- /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 00000000..3b798f9b --- /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 00000000..1a7713f2 --- /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 00000000..1441c03a --- /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 00000000..c58b9ffb --- /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 00000000..9cbb4b2c --- /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 00000000..fbdb3132 --- /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 00000000..57bca12f --- /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 00000000..c5d83ade --- /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 00000000..591b9726 --- /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 00000000..e6018520 --- /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 00000000..317d4286 --- /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 `. wkhtmltopdf + must be installed and path-accessible. + TODO: If HTML generation has run, reuse that output + TODO: Local web server to ease wkhtmltopdf rendering + */ + wkhtmltopdf: function(markup, fOut) { + var info, tempFile; + tempFile = fOut.replace(/\.pdf$/i, '.pdf.html'); + FS.writeFileSync(tempFile, markup, 'utf8'); + return info = SPAWN('wkhtmltopdf', [tempFile, fOut]); + }, + + /** + Generate a PDF from HTML using Phantom's CLI interface. + Spawns a child process with `phantomjs