(function() { /** Definition of the FRESHResume class. @license MIT. See LICENSE.md for details. @module core/fresh-resume */ /** 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. */ var CONVERTER, FS, FluentDate, 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'); FluentDate = require('./fluent-date'); /** 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 = class FreshResume { // extends AbstractResume /** Initialize the the FreshResume from JSON string data. */ parse(stringData, opts) { var ref; this.imp = (ref = this.imp) != null ? ref : { raw: stringData }; 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. } */ parseJSON(rep, opts) { var ignoreList, privateList, ref, scrubbed, scrubber; if (opts && opts.privatize) { // Ignore any element with the 'ignore: true' or 'private: true' designator. scrubber = require('../utils/resume-scrubber'); ({scrubbed, ignoreList, privateList} = scrubber.scrubResume(rep, opts)); } // Now apply the resume representation onto this object extend(true, this, opts && opts.privatize ? scrubbed : rep); if (!((ref = this.imp) != null ? ref.processed : void 0)) { // Set up metadata TODO: Clean up metadata on the object model. opts = opts || {}; if (opts.imp === void 0 || opts.imp) { this.imp = this.imp || {}; this.imp.title = (opts.title || this.imp.title) || this.name; if (!this.imp.raw) { this.imp.raw = JSON.stringify(rep); } } this.imp.processed = true; // Parse dates, sort dates, and calculate computed values (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). */ save(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. */ saveAs(filename, format) { var newRep, parts, safeFormat, useEdgeSchema; // If format isn't specified, default to FRESH safeFormat = (format && format.trim()) || 'FRESH'; // Validate against the FRESH version regex // freshVersionReg = require '../utils/fresh-version-regex' // if (not freshVersionReg().test( safeFormat )) // throw badVer: safeFormat parts = safeFormat.split('@'); if (parts[0] === 'FRESH') { this.imp.file = filename || this.imp.file; FS.writeFileSync(this.imp.file, this.stringify(), 'utf8'); } else if (parts[0] === 'JRS') { useEdgeSchema = parts.length > 1 ? parts[1] === '1' : false; newRep = CONVERTER.toJRS(this, { edge: useEdgeSchema }); FS.writeFileSync(filename, JRSResume.stringify(newRep), 'utf8'); } else { throw { badVer: safeFormat }; } 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. */ dupe() { 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. */ stringify() { 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. */ transformStrings(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. */ markdownify() { 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. */ xmlify() { var trx; trx = function(key, val) { return XML(val); }; return this.transformStrings([], trx); } /** Return the resume format. */ format() { return 'FRESH'; } /** Return internal metadata. Create if it doesn't exist. */ i() { return this.imp = this.imp || {}; } /** Return a unique list of all skills declared in the resume. */ // TODO: Several problems here: // 1) Confusing name. Easily confused with the keyword-inspector module, which // parses resume body text looking for these same keywords. This should probably // be renamed. // 2) Doesn't bother trying to integrate skills.list with skills.sets if they // happen to declare different skills, and if skills.sets declares ONE skill and // skills.list declared 50, only 1 skill will be registered. // 3) In the future, skill.sets should only be able to use skills declared in // skills.list. That is, skills.list is the official record of a candidate's // declared skills. skills.sets is just a way of grouping those into skillsets // for easier consumption. keywords() { 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 */ clear(clearMeta) { clearMeta = ((clearMeta === void 0) && true) || clearMeta; if (clearMeta) { delete this.imp; } delete this.computed; // Don't use Object.keys() here 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. */ count(obj) { if (!obj) { return 0; } if (obj.history) { return obj.history.length; } if (obj.sets) { return obj.sets.length; } return obj.length || 0; } add(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). */ hasProfile(socialNetwork) { socialNetwork = socialNetwork.trim().toLowerCase(); return this.social && _.some(this.social, function(p) { return p.network.trim().toLowerCase() === socialNetwork; }); } /** Return the specified network profile. */ getProfile(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. */ getProfiles(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. */ hasSkill(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. */ isValid(info) { var ret, schemaObj, validate; schemaObj = require('fresh-resume-schema'); validator = require('is-my-json-valid'); validate = validator(schemaObj, { // See Note [1]. 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; } duration(unit) { var inspector; inspector = require('../inspectors/duration-inspector'); return inspector.run(this, 'employment.history', 'start', 'end', unit); } /** Sort dated things on the sheet by start date descending. Assumes that dates on the sheet have been processed with _parseDates(). */ sort() { var byDateDesc, sortSection; byDateDesc = function(a, b) { if (a.safe.start.isBefore(b.safe.start)) { return 1; } else { if (a.safe.start.isAfter(b.safe.start)) { return -1; } else { return 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; } }); } }; /** Get the default (starter) sheet. */ FreshResume.default = function() { return new FreshResume().parseJSON(require('fresh-resume-starter').fresh); }; /** Convert the supplied FreshResume to a JSON string, sanitizing meta-properties along the way. */ FreshResume.stringify = function(obj) { var replacer; replacer = function(key, value) { // Exclude these keys from stringification 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); }; _parseDates = function() { var _fmt, replaceDatesInObject, that; _fmt = require('./fluent-date').fmt; that = this; // TODO: refactor recursion replaceDatesInObject = function(obj) { if (!obj) { return; } if (Object.prototype.toString.call(obj) === '[object Array]') { 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]); }); ['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) { obj.safe.end = _fmt('current'); } } }); } }; Object.keys(this).forEach(function(member) { replaceDatesInObject(that[member]); }); }; /** Export the Sheet function/ctor. */ module.exports = FreshResume; // Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats // in addition to YYYY-MM-DD. The original regex: // /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/ }).call(this); //# sourceMappingURL=fresh-resume.js.map