From 6ce2ae2391716c14007da884f0117df684ae3b95 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 18 Nov 2015 23:42:09 -0500 Subject: [PATCH 01/36] Introduce FRESH sheet class. Introduce the canonical FRESH sheet class based on the old HackMyResume (HMR) sources. Prepare to replace JSON Resume-specific handling with generic FRESH handling. --- src/core/fresh-sheet.js | 369 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 src/core/fresh-sheet.js diff --git a/src/core/fresh-sheet.js b/src/core/fresh-sheet.js new file mode 100644 index 0000000..bc0ca39 --- /dev/null +++ b/src/core/fresh-sheet.js @@ -0,0 +1,369 @@ +/** +FRESH character/resume sheet representation. +@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk +*/ + +(function() { + + var FS = require('fs') + , extend = require('../utils/extend') + , validator = require('is-my-json-valid') + , _ = require('underscore') + , PATH = require('path') + , moment = require('moment'); + + /** + A FRESH-style resume in JSON or YAML. + @class Sheet + */ + function FreshSheet() { + + } + + /** + Open and parse the specified JSON resume sheet. 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. + */ + FreshSheet.prototype.open = function( file, title ) { + this.meta = { fileName: file }; + this.meta.raw = FS.readFileSync( file, 'utf8' ); + return this.parse( this.meta.raw, title ); + }; + + /** + Save the sheet to disk (for environments that have disk access). + */ + FreshSheet.prototype.save = function( filename ) { + this.meta.fileName = filename || this.meta.fileName; + FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' ); + return this; + }; + + /** + Convert this object to a JSON string, sanitizing meta-properties along the + way. Don't override .toString(). + */ + FreshSheet.prototype.stringify = function() { + function replacer( key,value ) { // Exclude these keys from stringification + return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', + 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', + 'isModified', 'htmlPreview', 'display_progress_bar'], + function( val ) { return key.trim() === val; } + ) ? undefined : value; + } + return JSON.stringify( this, replacer, 2 ); + }; + + /** + Open and parse the specified JSON resume sheet. 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. + */ + FreshSheet.prototype.parse = function( stringData, opts ) { + + // Parse the incoming JSON representation + var rep = JSON.parse( stringData ); + + // Convert JSON Resume to FRESH if necessary + rep.basics && (rep = FreshSheet.convert( rep )); + + // Now apply the resume representation onto this object + extend( true, this, rep ); + + // Set up metadata + opts = opts || { }; + if( opts.meta === undefined || opts.meta ) { + this.meta = this.meta || { }; + this.meta.title = (opts.title || this.meta.title) || this.name; + } + // Parse dates, sort dates, and calculate computed values + (opts.date === undefined || opts.date) && _parseDates.call( this ); + (opts.sort === undefined || opts.sort) && this.sort(); + (opts.compute === undefined || opts.compute) && (this.computed = { + numYears: this.duration(), + keywords: this.keywords() + }); + return this; + }; + + /** + Convert from JSON Resume format + */ + FreshSheet.convert = function( jrs ) { + + return { + + name: jrs.basics.name, + label: jrs.basics.label, + class: jrs.basics.label, + summary: jrs.basics.summary, + + contact: { + email: jrs.basics.email, + phone: jrs.basics.phone, + website: jrs.basics.website, + postal: { + city: jrs.basics.location.city, + region: jrs.basics.location.region, + country: jrs.basics.location.countryCode, + code: jrs.basics.location.postalCode, + address: [ + jrs.basics.location.address, + ] + } + }, + + employment: { + history: jrs.work.map( function( job ) { + return { + position: job.position, + employer: job.company, + summary: job.summary, + current: !job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current', + start: job.startDate, + end: job.endDate, + url: job.website, + keywords: "", + highlights: job.highlights + }; + }) + }, + + education: { + history: jrs.education.map(function(edu){ + return { + institution: edu.institution, + start: edu.startDate, + end: edu.endDate, + grade: edu.gpa, + curriculum: edu.courses, + url: edu.website || edu.url || null, + summary: null, + // ???: edu.area, TODO + // ???: edu.studyType TODO + }; + }) + }, + + service: { + history: jrs.volunteer.map(function(vol) { + return { + type: 'volunteer', + position: vol.position, + organization: vol.organization, + start: vol.startDate, + end: vol.endDate, + url: vol.website, + summary: vol.summary, + highlights: vol.highlights + }; + }) + }, + + skills: jrs.skills.map(function(sk){ + return { + name: sk.name, + summary: "", + level: sk.level, + summary: sk.keywords.join(', '), + years: null, + proof: null + }; + }), + + publications: jrs.publications.map(function(pub){ + return { + title: pub.name, + publisher: pub.publisher, + link: [ + { 'url': pub.website } + ], + year: pub.releaseDate + }; + }), + + interests: jrs.interests + }; + }; + + /** + Return a unique list of all keywords across all skills. + */ + FreshSheet.prototype.keywords = function() { + var flatSkills = []; + this.skills && this.skills.length && + (flatSkills = this.skills.map(function(sk) { return sk.name; })); + return flatSkills; + }, + + /** + Update the sheet's raw data. TODO: remove/refactor + */ + FreshSheet.prototype.updateData = function( str ) { + this.clear( false ); + this.parse( str ) + return this; + }; + + /** + Reset the sheet to an empty state. + */ + FreshSheet.prototype.clear = function( clearMeta ) { + clearMeta = ((clearMeta === undefined) && true) || clearMeta; + clearMeta && (delete this.meta); + delete this.computed; // Don't use Object.keys() here + delete this.employment; + delete this.service; + delete this.education; + //delete this.awards; + delete this.publications; + //delete this.interests; + delete this.skills; + delete this.social; + }; + + /** + Get the default (empty) sheet. + */ + FreshSheet.default = function() { + return new FreshSheet().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); + } + + /** + Add work experience to the sheet. + */ + FreshSheet.prototype.add = function( moniker ) { + var defSheet = FreshSheet.default(); + var 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). + */ + FreshSheet.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. + */ + FreshSheet.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. + */ + FreshSheet.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓ + var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' ); + var schemaObj = JSON.parse( schema ); + var validator = require('is-my-json-valid') + var validate = validator( schemaObj ); + return validate( this ); + }; + + /** + 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. + */ + FreshSheet.prototype.duration = function() { + if( this.employment.history && this.employment.history.length ) { + var careerStart = this.employment.history[ this.employment.history.length - 1].safe.start; + if ((typeof careerStart === 'string' || careerStart instanceof String) && + !careerStart.trim()) + return 0; + var careerLast = _.max( this.employment.history, function( w ) { + return w.safe.end.unix(); + }).safe.end; + return careerLast.diff( careerStart, 'years' ); + } + return 0; + }; + + /** + Sort dated things on the sheet by start date descending. Assumes that dates + on the sheet have been processed with _parseDates(). + */ + FreshSheet.prototype.sort = function( ) { + + this.employment.history && this.employment.history.sort( byDateDesc ); + this.education.history && this.education.history.sort( byDateDesc ); + this.service.history && this.service.history.sort( byDateDesc ); + + // this.awards && this.awards.sort( function(a, b) { + // return( a.safeDate.isBefore(b.safeDate) ) ? 1 + // : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0; + // }); + this.publications && this.publications.sort( function(a, b) { + return( a.safe.date.isBefore(b.safe.date) ) ? 1 + : ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0; + }); + + function byDateDesc(a,b) { + return( a.safe.start.isBefore(b.safe.start) ) ? 1 + : ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0; + } + + }; + + /** + 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. + */ + function _parseDates() { + + var _fmt = require('./fluent-date').fmt; + + this.employment.history && this.employment.history.forEach( function(job) { + job.safe = { + start: _fmt( job.start ), + end: _fmt( job.end || 'current' ) + }; + }); + this.education.history && this.education.history.forEach( function(edu) { + edu.safe = { + start: _fmt( edu.start ), + end: _fmt( edu.end || 'current' ) + }; + }); + this.service.history && this.service.history.forEach( function(vol) { + vol.safe = { + start: _fmt( vol.start ), + end: _fmt( vol.end || 'current' ) + }; + }); + // this.awards && this.awards.forEach( function(awd) { + // awd.safeDate = _fmt( awd.date ); + // }); + this.publications && this.publications.forEach( function(pub) { + pub.safe = { + date: _fmt( pub.year ) + }; + }); + } + + /** + Export the Sheet function/ctor. + */ + module.exports = FreshSheet; + +}()); From f3eb46a154f716e55d7b7233fd38d9943d39cff9 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 18 Nov 2015 23:44:16 -0500 Subject: [PATCH 02/36] Add starter tests for FRESH sheet. --- tests/test-fresh-sheet.js | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/test-fresh-sheet.js diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js new file mode 100644 index 0000000..4df7db7 --- /dev/null +++ b/tests/test-fresh-sheet.js @@ -0,0 +1,68 @@ + +var chai = require('chai') + , expect = chai.expect + , should = chai.should() + , path = require('path') + , _ = require('underscore') + , FreshSheet = require('../src/core/fresh-sheet') + , validator = require('is-my-json-valid'); + +chai.config.includeStack = false; + +describe('fullstack.json (FRESH)', function () { + + var _sheet; + + it('should open without throwing an exception', function () { + function tryOpen() { + _sheet = new FreshSheet().open( 'tests/exemplars/fresh-exemplar.json' ); + } + tryOpen.should.not.Throw(); + }); + + it('should have one or more of each section', function() { + expect( + //(_sheet.basics) && + (_sheet.name && _sheet.label && _sheet.class && _sheet.summary) && + (_sheet.employment.history && _sheet.employment.history.length > 0) && + (_sheet.skills && _sheet.skills.length > 0) && + (_sheet.education.history && _sheet.education.history.length > 0) && + (_sheet.service.history && _sheet.service.history.length > 0) && + (_sheet.publications && _sheet.publications.length > 0) //&& + //(_sheet.awards && _sheet.awards.length > 0) + ).to.equal( true ); + }); + + it('should have a work duration of 7 years', function() { + _sheet.computed.numYears.should.equal( 7 ); + }); + + it('should save without throwing an exception', function(){ + function trySave() { + _sheet.save( 'tests/sandbox/fullstack.json' ); + } + trySave.should.not.Throw(); + }); + + it('should not be modified after saving', function() { + var savedSheet = new FreshSheet().open( 'tests/sandbox/fullstack.json' ); + _sheet.stringify().should.equal( savedSheet.stringify() ) + }); + + // it('should validate against the FRESH resume schema', function() { + // var schemaJson = require('../src/core/resume.json'); + // var validate = validator( schemaJson, { verbose: true } ); + // var result = validate( JSON.parse( _sheet.meta.raw ) ); + // result || console.log("\n\nOops, resume didn't validate. " + + // "Validation errors:\n\n", validate.errors, "\n\n"); + // result.should.equal( true ); + // }); + + +}); + +// describe('subtract', function () { +// it('should return -1 when passed the params (1, 2)', function () { +// expect(math.subtract(1, 2)).to.equal(-1); +// }); +// }); From 0bebd87bd6781ab122f51f5de084fbff7aeefe02 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 18 Nov 2015 23:44:38 -0500 Subject: [PATCH 03/36] Rename JSON Resume test sheet to test-jrs-sheet.js. --- tests/{test-sheet.js => test-jrs-sheet.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{test-sheet.js => test-jrs-sheet.js} (97%) diff --git a/tests/test-sheet.js b/tests/test-jrs-sheet.js similarity index 97% rename from tests/test-sheet.js rename to tests/test-jrs-sheet.js index 454a933..aa40c95 100644 --- a/tests/test-sheet.js +++ b/tests/test-jrs-sheet.js @@ -9,7 +9,7 @@ var chai = require('chai') chai.config.includeStack = false; -describe('fullstack.json', function () { +describe('fullstack.json (JRS)', function () { var _sheet; From 30b6bc4d807d8f5dee0db14057de6f4276a7e7fa Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 01:47:23 -0500 Subject: [PATCH 04/36] Remove invalid object model reference. --- src/core/fresh-sheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/fresh-sheet.js b/src/core/fresh-sheet.js index bc0ca39..ce72b45 100644 --- a/src/core/fresh-sheet.js +++ b/src/core/fresh-sheet.js @@ -246,7 +246,7 @@ FRESH character/resume sheet representation. */ FreshSheet.prototype.hasProfile = function( socialNetwork ) { socialNetwork = socialNetwork.trim().toLowerCase(); - return this.basics.profiles && _.some( this.basics.profiles, function(p) { + return this.social && _.some( this.social, function(p) { return p.network.trim().toLowerCase() === socialNetwork; }); }; From ce955930310349060c0d1ec3107fa1c7eab5fb70 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 01:57:15 -0500 Subject: [PATCH 05/36] Relax copyright notices. --- src/core/sheet.js | 2 +- src/fluentcmd.js | 2 +- src/fluentlib.js | 4 ++-- src/index.js | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/sheet.js b/src/core/sheet.js index 9d8e666..6cd1b7f 100644 --- a/src/core/sheet.js +++ b/src/core/sheet.js @@ -1,6 +1,6 @@ /** Abstract character/resume sheet representation. -@license Copyright (c) 2015 by James M. Devlin. All rights reserved. +@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk */ (function() { diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 92df228..1fedabc 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -1,6 +1,6 @@ /** Internal resume generation logic for FluentCV. -@license Copyright (c) 2015 | James M. Devlin +@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk */ module.exports = function () { diff --git a/src/fluentlib.js b/src/fluentlib.js index ddb4fd2..1476c24 100644 --- a/src/fluentlib.js +++ b/src/fluentlib.js @@ -1,6 +1,6 @@ /** -Core resume generation module for FluentCV. -@license Copyright (c) 2015 by James M. Devlin. All rights reserved. +External API surface for FluentCV:CLI. +@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk */ module.exports = { diff --git a/src/index.js b/src/index.js index ab98bf0..23d9ad3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,8 @@ #! /usr/bin/env node /** -Command-line interface (CLI) for FluentCV via Node.js. -@license Copyright (c) 2015 | James M. Devlin +Command-line interface (CLI) for FluentCV:CLI. +@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk. */ var ARGS = require( 'minimist' ) From b167abcb78cb905a8c07ec16b24d70d89ab8f64e Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 01:57:43 -0500 Subject: [PATCH 06/36] Bump version to 0.9.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b07f9ce..bac13db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fluentcv", - "version": "0.8.0", + "version": "0.9.0", "description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.", "repository": { "type": "git", From 9044dff504ea7c5b40a2cab5acb9aa10deedc663 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 09:39:49 -0500 Subject: [PATCH 07/36] Introduce FRESH and JSONResume conversion routines. --- src/core/convert.js | 244 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 src/core/convert.js diff --git a/src/core/convert.js b/src/core/convert.js new file mode 100644 index 0000000..3f7a0e5 --- /dev/null +++ b/src/core/convert.js @@ -0,0 +1,244 @@ +/** +FRESH to JSON Resume conversion routiens. +@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk +*/ + +(function(){ + + /** + Convert between FRESH and JRS resume/CV formats. + @class FRESHConverter + */ + var FRESHConverter = module.exports = { + + + /** + Convert from JSON Resume format to FRESH. + */ + toFRESH: function( jrs ) { + + return { + + name: jrs.basics.name, + + info: { + label: jrs.basics.label, + class: jrs.basics.label, + picture: jrs.basics.picture, + summary: jrs.basics.summary + }, + + contact: { + email: jrs.basics.email, + phone: jrs.basics.phone, + website: jrs.basics.website + }, + + location: { + city: jrs.basics.location.city, + region: jrs.basics.location.region, + country: jrs.basics.location.countryCode, + code: jrs.basics.location.postalCode, + address: [ + jrs.basics.location.address, + ] + }, + + employment: { + history: jrs.work.map( function( job ) { + return { + position: job.position, + employer: job.company, + summary: job.summary, + current: !job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current', + start: job.startDate, + end: job.endDate, + url: job.website, + keywords: "", + highlights: job.highlights + }; + }) + }, + + education: { + history: jrs.education.map(function(edu){ + return { + institution: edu.institution, + start: edu.startDate, + end: edu.endDate, + grade: edu.gpa, + curriculum: edu.courses, + url: edu.website || edu.url || null, + summary: null, + // ???: edu.area, TODO + // ???: edu.studyType TODO + }; + }) + }, + + service: { + history: jrs.volunteer.map(function(vol) { + return { + type: 'volunteer', + position: vol.position, + organization: vol.organization, + start: vol.startDate, + end: vol.endDate, + url: vol.website, + summary: vol.summary, + highlights: vol.highlights + }; + }) + }, + + skills: jrs.skills.map(function(sk){ + return { + name: sk.name, + summary: "", + level: sk.level, + summary: sk.keywords.join(', '), + years: null, + proof: null + }; + }), + + publications: jrs.publications.map(function(pub){ + return { + title: pub.name, + publisher: pub.publisher, + link: [ + { 'url': pub.website } + ], + year: pub.releaseDate + }; + }), + + recognition: jrs.awards.map(function(awd){ + return { + title: awd.title, + date: awd.date, + summary: awd.summary, + from: awd.awarder, + url: null + }; + }), + + social: jrs.basics.profiles.map(function(pro){ + return { + label: pro.network, + network: pro.network, + url: pro.url, + user: pro.username + }; + }), + + interests: jrs.interests + }; + }, + + /** + Convert from FRESH format to JSON Resume. + */ + toJRS: function( fresh ) { + + return { + + basics: { + name: fresh.name, + summary: fresh.info.summary, + website: fresh.info.website, + phone: fresh.info.phone, + email: fresh.info.email, + picture: fresh.info.picture, + location: { + address: fresh.location.address.join('\n'), + postalCode: fresh.location.code, + city: fresh.location.city, + countryCode: fresh.location.country, + region: fresh.location.region + }, + profiles: fresh.social.map(function(soc){ + return { + network: soc.network, + username: soc.user, + url: soc.url + }; + }) + }, + + work: fresh.employment.history.map(function(emp){ + return { + company: emp.employer, + position: emp.position, + startDate: emp.start, + endDate: emp.end, + summary: emp.summary, + highlights: emp.highlights + }; + }), + + education: fresh.education.history.map(function(edu){ + return { + institution: edu.institution, + gpa: edu.grade, + courses: edu.curriculum, + startDate: edu.start, + endDate: edu.end, + area: "", // TODO + studyType: "" + }; + }), + + skills: fresh.skills.map( function(sk){ + return { + name: sk.name, + level: sk.level, + keywords: [], // TODO + //???: sk.years, + //???: sk.summary + }; + }), + + volunteer: fresh.service.history.map(function(srv){ + return { + //???: srv.type, + organization: srv.organization, + position: srv.position, + startDate: srv.start, + endDate: srv.end, + website: srv.url, + summary: srv.summary, + highlights: srv.highlights + }; + }), + + awards: fresh.recognition.map(function(awd){ + return { + //???: awd.type, // TODO + //???: awd.url, + title: awd.title, + date: awd.date, + awarder: awd.from, + summary: awd.summary + }; + }), + + publications: fresh.publications.map(function(pub){ + return { + name: pub.title, + publisher: "", // TODO + releaseDate: pub.date, + website: pub.link[0].url, + summary: pub.summary + }; + }), + + interests: fresh.interests + + }; + + } + + }; + +}()); From a410153253780e2056e24b700ba05b6fd863aadb Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 09:46:02 -0500 Subject: [PATCH 08/36] Implement "generate" and "validate" verbs. Start moving to a more familiar verb-based interface with "generate" and "validate" commands. Use with "fluentcv generate" or "fluentcv validate". --- src/core/fresh-sheet.js | 118 ++++---------------------------------- src/fluentcmd.js | 51 +++++++++++++++- src/fluentlib.js | 2 +- src/gen/base-generator.js | 4 +- src/index.js | 28 +++++---- 5 files changed, 83 insertions(+), 120 deletions(-) diff --git a/src/core/fresh-sheet.js b/src/core/fresh-sheet.js index ce72b45..ac80135 100644 --- a/src/core/fresh-sheet.js +++ b/src/core/fresh-sheet.js @@ -10,7 +10,8 @@ FRESH character/resume sheet representation. , validator = require('is-my-json-valid') , _ = require('underscore') , PATH = require('path') - , moment = require('moment'); + , moment = require('moment') + , CONVERTER = require('./convert'); /** A FRESH-style resume in JSON or YAML. @@ -66,7 +67,7 @@ FRESH character/resume sheet representation. var rep = JSON.parse( stringData ); // Convert JSON Resume to FRESH if necessary - rep.basics && (rep = FreshSheet.convert( rep )); + rep.basics && (rep = CONVERTER.toFRESH( rep )); // Now apply the resume representation onto this object extend( true, this, rep ); @@ -87,106 +88,6 @@ FRESH character/resume sheet representation. return this; }; - /** - Convert from JSON Resume format - */ - FreshSheet.convert = function( jrs ) { - - return { - - name: jrs.basics.name, - label: jrs.basics.label, - class: jrs.basics.label, - summary: jrs.basics.summary, - - contact: { - email: jrs.basics.email, - phone: jrs.basics.phone, - website: jrs.basics.website, - postal: { - city: jrs.basics.location.city, - region: jrs.basics.location.region, - country: jrs.basics.location.countryCode, - code: jrs.basics.location.postalCode, - address: [ - jrs.basics.location.address, - ] - } - }, - - employment: { - history: jrs.work.map( function( job ) { - return { - position: job.position, - employer: job.company, - summary: job.summary, - current: !job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current', - start: job.startDate, - end: job.endDate, - url: job.website, - keywords: "", - highlights: job.highlights - }; - }) - }, - - education: { - history: jrs.education.map(function(edu){ - return { - institution: edu.institution, - start: edu.startDate, - end: edu.endDate, - grade: edu.gpa, - curriculum: edu.courses, - url: edu.website || edu.url || null, - summary: null, - // ???: edu.area, TODO - // ???: edu.studyType TODO - }; - }) - }, - - service: { - history: jrs.volunteer.map(function(vol) { - return { - type: 'volunteer', - position: vol.position, - organization: vol.organization, - start: vol.startDate, - end: vol.endDate, - url: vol.website, - summary: vol.summary, - highlights: vol.highlights - }; - }) - }, - - skills: jrs.skills.map(function(sk){ - return { - name: sk.name, - summary: "", - level: sk.level, - summary: sk.keywords.join(', '), - years: null, - proof: null - }; - }), - - publications: jrs.publications.map(function(pub){ - return { - title: pub.name, - publisher: pub.publisher, - link: [ - { 'url': pub.website } - ], - year: pub.releaseDate - }; - }), - - interests: jrs.interests - }; - }; - /** Return a unique list of all keywords across all skills. */ @@ -266,12 +167,17 @@ FRESH character/resume sheet representation. /** Validate the sheet against the FRESH Resume schema. */ - FreshSheet.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓ - var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' ); - var schemaObj = JSON.parse( schema ); + FreshSheet.prototype.isValid = function( info ) { + var schemaObj = require('FRESCA'); + //var schemaObj = JSON.parse( schema ); var validator = require('is-my-json-valid') var validate = validator( schemaObj ); - return validate( this ); + var ret = validate( this ); + if( !ret ) { + this.meta = this.meta || { }; + this.meta.validationErrors = validate.errors; + } + return ret; }; /** diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 1fedabc..29f7455 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -9,7 +9,7 @@ module.exports = function () { var path = require( 'path' ) , extend = require( './utils/extend' ) , unused = require('./utils/string') - , fs = require('fs') + , FS = require('fs') , _ = require('underscore') , FLUENT = require('./fluentlib') , PATH = require('path') @@ -109,6 +109,46 @@ module.exports = function () { throw ex; } + /** + Validate 1 to N resumes as vanilla JSON. + */ + // function validateAsJSON( src, logger ) { + // _log = logger || console.log; + // if( !src || !src.length ) { throw { fluenterror: 3 }; } + // var isValid = true; + // var sheets = src.map( function( res ) { + // try { + // var rawJson = FS.readFileSync( res, 'utf8' ); + // var testObj = JSON.parse( rawJson ); + // } + // catch(ex) { + // if (!(ex instanceof SyntaxError)) { throw ex; } // [1] + // isValid = false; + // } + // + // _log( 'Validating JSON resume: ' + res + (isValid ? ' (VALID)' : ' (INVALID)')); + // return isValid; + // }); + // } + + /** + Validate 1 to N resumes in either FRESH or JSON Resume format. + */ + function validate( src, unused, opts, logger ) { + _log = logger || console.log; + if( !src || !src.length ) { throw { fluenterror: 3 }; } + var isValid = true; + var sheets = src.map( function( res ) { + var sheet = (new FLUENT.Sheet()).open( res ); + var valid = sheet.isValid(); + _log( 'Validating JSON resume: ' + res + + (valid ? ' (VALID)' : ' (INVALID)')); + if( !valid ) { + _log( sheet.meta.validationErrors ); + } + }); + } + /** Supported resume formats. */ @@ -139,10 +179,17 @@ module.exports = function () { Internal module interface. Used by FCV Desktop and HMR. */ return { - generate: gen, + verbs: { + generate: gen, + validate: validate, + convert: convert + }, lib: require('./fluentlib'), options: _opts, formats: _fmts }; }(); + +// [1]: JSON.parse throws SyntaxError on invalid JSON. See: +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse diff --git a/src/fluentlib.js b/src/fluentlib.js index 1476c24..226b693 100644 --- a/src/fluentlib.js +++ b/src/fluentlib.js @@ -4,7 +4,7 @@ External API surface for FluentCV:CLI. */ module.exports = { - Sheet: require('./core/sheet'), + Sheet: require('./core/fresh-sheet'), Theme: require('./core/theme'), FluentDate: require('./core/fluent-date'), HtmlGenerator: require('./gen/html-generator'), diff --git a/src/gen/base-generator.js b/src/gen/base-generator.js index 0a239f1..86bc452 100644 --- a/src/gen/base-generator.js +++ b/src/gen/base-generator.js @@ -29,7 +29,9 @@ Base resume generator for FluentCV. success: 0, themeNotFound: 1, copyCss: 2, - resumeNotFound: 3 + resumeNotFound: 3, + missingCommand: 4, + invalidCommand: 5 }, /** diff --git a/src/index.js b/src/index.js index 23d9ad3..b528898 100644 --- a/src/index.js +++ b/src/index.js @@ -23,20 +23,27 @@ catch( ex ) { function main() { - // Setup. + // Setup var title = '*** FluentCV v' + PKG.version + ' ***'; - if( process.argv.length <= 2 ) { logMsg(title); throw { fluenterror: 3 }; } - var args = ARGS( process.argv.slice(2) ); - opts = getOpts( args ); + if( process.argv.length <= 2 ) { logMsg(title); throw { fluenterror: 4 }; } + var a = ARGS( process.argv.slice(2) ); + opts = getOpts( a ); logMsg( title ); - // Convert arguments to source files, target files, options - var src = args._ || []; - var dst = (args.o && ((typeof args.o === 'string' && [ args.o ]) || args.o)) || []; - dst = (dst === true) ? [] : dst; // Handle -o with missing output file + // Get the action to be performed + var verb = a._[0].toLowerCase().trim(); + if( !FCMD.verbs[ verb ] ) { + throw 'Invalid command: "' + verb + '"'; + } + + // Preload our params array + var dst = (a.o && ((typeof a.o === 'string' && [ a.o ]) || a.o)) || []; + dst = (dst === true) ? [] : dst; // Handle -o with missing output file + var parms = [ a._.slice(1) || [], dst, opts, logMsg ]; + + // Invoke the action + FCMD.verbs[ verb ].apply( null, parms ); - // Generate! - FCMD.generate( src, dst, opts, logMsg ); } function logMsg( msg ) { @@ -60,6 +67,7 @@ function handleError( ex ) { case 1: msg = "The specified theme couldn't be found: " + ex.data; break; case 2: msg = "Couldn't copy CSS file to destination folder"; break; case 3: msg = "Please specify a valid JSON resume file."; break; + case 4: msg = "Please specify a valid command (GENERATE, VALIDATE, or CONVERT)." }; exitCode = ex.fluenterror; } From 0aa9bc29372853eabebf5dd5ef5a47c4e3e32b75 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 10:39:14 -0500 Subject: [PATCH 09/36] Rename Sheet/FreshSheet to JRSResume/FRESHResume. --- src/core/{fresh-sheet.js => fresh-resume.js} | 45 ++++++++++---------- src/core/{sheet.js => jrs-resume.js} | 44 +++++++++---------- src/fluentcmd.js | 4 +- src/fluentlib.js | 4 +- tests/test-fresh-sheet.js | 6 +-- tests/test-jrs-sheet.js | 6 +-- 6 files changed, 55 insertions(+), 54 deletions(-) rename src/core/{fresh-sheet.js => fresh-resume.js} (86%) rename src/core/{sheet.js => jrs-resume.js} (87%) diff --git a/src/core/fresh-sheet.js b/src/core/fresh-resume.js similarity index 86% rename from src/core/fresh-sheet.js rename to src/core/fresh-resume.js index ac80135..137b1e4 100644 --- a/src/core/fresh-sheet.js +++ b/src/core/fresh-resume.js @@ -1,5 +1,5 @@ /** -FRESH character/resume sheet representation. +Definition of the FRESHResume class. @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk */ @@ -15,18 +15,18 @@ FRESH character/resume sheet representation. /** A FRESH-style resume in JSON or YAML. - @class Sheet + @class FreshResume */ - function FreshSheet() { + function FreshResume() { } /** - Open and parse the specified JSON resume sheet. Merge the JSON object model + Open and parse the specified FRESH resume sheet. 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. */ - FreshSheet.prototype.open = function( file, title ) { + FreshResume.prototype.open = function( file, title ) { this.meta = { fileName: file }; this.meta.raw = FS.readFileSync( file, 'utf8' ); return this.parse( this.meta.raw, title ); @@ -35,7 +35,7 @@ FRESH character/resume sheet representation. /** Save the sheet to disk (for environments that have disk access). */ - FreshSheet.prototype.save = function( filename ) { + FreshResume.prototype.save = function( filename ) { this.meta.fileName = filename || this.meta.fileName; FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' ); return this; @@ -45,11 +45,10 @@ FRESH character/resume sheet representation. Convert this object to a JSON string, sanitizing meta-properties along the way. Don't override .toString(). */ - FreshSheet.prototype.stringify = function() { + FreshResume.prototype.stringify = function() { function replacer( key,value ) { // Exclude these keys from stringification return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', - 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', - 'isModified', 'htmlPreview', 'display_progress_bar'], + 'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'], function( val ) { return key.trim() === val; } ) ? undefined : value; } @@ -61,7 +60,7 @@ FRESH character/resume sheet representation. onto this Sheet instance with extend() and convert sheet dates to a safe & consistent format. Then sort each section by startDate descending. */ - FreshSheet.prototype.parse = function( stringData, opts ) { + FreshResume.prototype.parse = function( stringData, opts ) { // Parse the incoming JSON representation var rep = JSON.parse( stringData ); @@ -91,7 +90,7 @@ FRESH character/resume sheet representation. /** Return a unique list of all keywords across all skills. */ - FreshSheet.prototype.keywords = function() { + FreshResume.prototype.keywords = function() { var flatSkills = []; this.skills && this.skills.length && (flatSkills = this.skills.map(function(sk) { return sk.name; })); @@ -101,7 +100,7 @@ FRESH character/resume sheet representation. /** Update the sheet's raw data. TODO: remove/refactor */ - FreshSheet.prototype.updateData = function( str ) { + FreshResume.prototype.updateData = function( str ) { this.clear( false ); this.parse( str ) return this; @@ -110,7 +109,7 @@ FRESH character/resume sheet representation. /** Reset the sheet to an empty state. */ - FreshSheet.prototype.clear = function( clearMeta ) { + FreshResume.prototype.clear = function( clearMeta ) { clearMeta = ((clearMeta === undefined) && true) || clearMeta; clearMeta && (delete this.meta); delete this.computed; // Don't use Object.keys() here @@ -127,15 +126,15 @@ FRESH character/resume sheet representation. /** Get the default (empty) sheet. */ - FreshSheet.default = function() { - return new FreshSheet().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); + FreshResume.default = function() { + return new FreshResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); } /** Add work experience to the sheet. */ - FreshSheet.prototype.add = function( moniker ) { - var defSheet = FreshSheet.default(); + FreshResume.prototype.add = function( moniker ) { + var defSheet = FreshResume.default(); var newObject = $.extend( true, {}, defSheet[ moniker ][0] ); this[ moniker ] = this[ moniker ] || []; this[ moniker ].push( newObject ); @@ -145,7 +144,7 @@ FRESH character/resume sheet representation. /** Determine if the sheet includes a specific social profile (eg, GitHub). */ - FreshSheet.prototype.hasProfile = function( socialNetwork ) { + FreshResume.prototype.hasProfile = function( socialNetwork ) { socialNetwork = socialNetwork.trim().toLowerCase(); return this.social && _.some( this.social, function(p) { return p.network.trim().toLowerCase() === socialNetwork; @@ -155,7 +154,7 @@ FRESH character/resume sheet representation. /** Determine if the sheet includes a specific skill. */ - FreshSheet.prototype.hasSkill = function( 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) { @@ -167,7 +166,7 @@ FRESH character/resume sheet representation. /** Validate the sheet against the FRESH Resume schema. */ - FreshSheet.prototype.isValid = function( info ) { + FreshResume.prototype.isValid = function( info ) { var schemaObj = require('FRESCA'); //var schemaObj = JSON.parse( schema ); var validator = require('is-my-json-valid') @@ -188,7 +187,7 @@ FRESH character/resume sheet representation. *latest end date of all jobs in the work history*. This last condition is for sheets that have overlapping jobs. */ - FreshSheet.prototype.duration = function() { + FreshResume.prototype.duration = function() { if( this.employment.history && this.employment.history.length ) { var careerStart = this.employment.history[ this.employment.history.length - 1].safe.start; if ((typeof careerStart === 'string' || careerStart instanceof String) && @@ -206,7 +205,7 @@ FRESH character/resume sheet representation. Sort dated things on the sheet by start date descending. Assumes that dates on the sheet have been processed with _parseDates(). */ - FreshSheet.prototype.sort = function( ) { + FreshResume.prototype.sort = function( ) { this.employment.history && this.employment.history.sort( byDateDesc ); this.education.history && this.education.history.sort( byDateDesc ); @@ -270,6 +269,6 @@ FRESH character/resume sheet representation. /** Export the Sheet function/ctor. */ - module.exports = FreshSheet; + module.exports = FreshResume; }()); diff --git a/src/core/sheet.js b/src/core/jrs-resume.js similarity index 87% rename from src/core/sheet.js rename to src/core/jrs-resume.js index 6cd1b7f..55e8e29 100644 --- a/src/core/sheet.js +++ b/src/core/jrs-resume.js @@ -1,5 +1,5 @@ /** -Abstract character/resume sheet representation. +Definition of the JRSResume class. @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk */ @@ -13,14 +13,14 @@ Abstract character/resume sheet representation. , moment = require('moment'); /** - The Sheet class represent a specific JSON character sheet. When Sheet.open + The JRSResume class represent a specific JSON character sheet. When Sheet.open is called, we merge the loaded JSON sheet properties onto the Sheet instance via extend(), so a full-grown sheet object will have all of the methods here, plus a complement of JSON properties from the backing JSON file. That allows us to treat Sheet objects interchangeably with the loaded JSON model. - @class Sheet + @class JRSResume */ - function Sheet() { + function JRSResume() { } @@ -29,7 +29,7 @@ Abstract character/resume sheet representation. onto this Sheet instance with extend() and convert sheet dates to a safe & consistent format. Then sort each section by startDate descending. */ - Sheet.prototype.open = function( file, title ) { + JRSResume.prototype.open = function( file, title ) { this.meta = { fileName: file }; this.meta.raw = FS.readFileSync( file, 'utf8' ); return this.parse( this.meta.raw, title ); @@ -38,7 +38,7 @@ Abstract character/resume sheet representation. /** Save the sheet to disk (for environments that have disk access). */ - Sheet.prototype.save = function( filename ) { + JRSResume.prototype.save = function( filename ) { this.meta.fileName = filename || this.meta.fileName; FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' ); return this; @@ -48,7 +48,7 @@ Abstract character/resume sheet representation. Convert this object to a JSON string, sanitizing meta-properties along the way. Don't override .toString(). */ - Sheet.prototype.stringify = function() { + JRSResume.prototype.stringify = function() { function replacer( key,value ) { // Exclude these keys from stringification return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', @@ -64,7 +64,7 @@ Abstract character/resume sheet representation. onto this Sheet instance with extend() and convert sheet dates to a safe & consistent format. Then sort each section by startDate descending. */ - Sheet.prototype.parse = function( stringData, opts ) { + JRSResume.prototype.parse = function( stringData, opts ) { opts = opts || { }; var rep = JSON.parse( stringData ); extend( true, this, rep ); @@ -86,7 +86,7 @@ Abstract character/resume sheet representation. /** Return a unique list of all keywords across all skills. */ - Sheet.prototype.keywords = function() { + JRSResume.prototype.keywords = function() { var flatSkills = []; if( this.skills && this.skills.length ) { this.skills.forEach( function( s ) { @@ -99,7 +99,7 @@ Abstract character/resume sheet representation. /** Update the sheet's raw data. TODO: remove/refactor */ - Sheet.prototype.updateData = function( str ) { + JRSResume.prototype.updateData = function( str ) { this.clear( false ); this.parse( str ) return this; @@ -108,7 +108,7 @@ Abstract character/resume sheet representation. /** Reset the sheet to an empty state. */ - Sheet.prototype.clear = function( clearMeta ) { + JRSResume.prototype.clear = function( clearMeta ) { clearMeta = ((clearMeta === undefined) && true) || clearMeta; clearMeta && (delete this.meta); delete this.computed; // Don't use Object.keys() here @@ -125,15 +125,15 @@ Abstract character/resume sheet representation. /** Get the default (empty) sheet. */ - Sheet.default = function() { - return new Sheet().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); + JRSResume.default = function() { + return new JRSResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); } /** Add work experience to the sheet. */ - Sheet.prototype.add = function( moniker ) { - var defSheet = Sheet.default(); + JRSResume.prototype.add = function( moniker ) { + var defSheet = JRSResume.default(); var newObject = $.extend( true, {}, defSheet[ moniker ][0] ); this[ moniker ] = this[ moniker ] || []; this[ moniker ].push( newObject ); @@ -143,7 +143,7 @@ Abstract character/resume sheet representation. /** Determine if the sheet includes a specific social profile (eg, GitHub). */ - Sheet.prototype.hasProfile = function( socialNetwork ) { + 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; @@ -153,7 +153,7 @@ Abstract character/resume sheet representation. /** Determine if the sheet includes a specific skill. */ - Sheet.prototype.hasSkill = function( 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) { @@ -165,7 +165,7 @@ Abstract character/resume sheet representation. /** Validate the sheet against the JSON Resume schema. */ - Sheet.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓ + JRSResume.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓ var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' ); var schemaObj = JSON.parse( schema ); var validator = require('is-my-json-valid') @@ -181,7 +181,7 @@ Abstract character/resume sheet representation. *latest end date of all jobs in the work history*. This last condition is for sheets that have overlapping jobs. */ - Sheet.prototype.duration = function() { + JRSResume.prototype.duration = function() { if( this.work && this.work.length ) { var careerStart = this.work[ this.work.length - 1].safeStartDate; if ((typeof careerStart === 'string' || careerStart instanceof String) && @@ -199,7 +199,7 @@ Abstract character/resume sheet representation. Sort dated things on the sheet by start date descending. Assumes that dates on the sheet have been processed with _parseDates(). */ - Sheet.prototype.sort = function( ) { + JRSResume.prototype.sort = function( ) { this.work && this.work.sort( byDateDesc ); this.education && this.education.sort( byDateDesc ); @@ -253,8 +253,8 @@ Abstract character/resume sheet representation. } /** - Export the Sheet function/ctor. + Export the JRSResume function/ctor. */ - module.exports = Sheet; + module.exports = JRSResume; }()); diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 29f7455..ebbfded 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -37,7 +37,7 @@ module.exports = function () { if(!src || !src.length) { throw { fluenterror: 3 }; } var sheets = src.map( function( res ) { _log( 'Reading JSON resume: ' + res ); - return (new FLUENT.Sheet()).open( res ); + return (new FLUENT.FRESHResume()).open( res ); }); // Merge input resumes... @@ -139,7 +139,7 @@ module.exports = function () { if( !src || !src.length ) { throw { fluenterror: 3 }; } var isValid = true; var sheets = src.map( function( res ) { - var sheet = (new FLUENT.Sheet()).open( res ); + var sheet = (new FLUENT.FRESHResume()).open( res ); var valid = sheet.isValid(); _log( 'Validating JSON resume: ' + res + (valid ? ' (VALID)' : ' (INVALID)')); diff --git a/src/fluentlib.js b/src/fluentlib.js index 226b693..26e23b7 100644 --- a/src/fluentlib.js +++ b/src/fluentlib.js @@ -4,7 +4,9 @@ External API surface for FluentCV:CLI. */ module.exports = { - Sheet: require('./core/fresh-sheet'), + Sheet: require('./core/fresh-resume'), + FRESHResume: require('./core/fresh-resume'), + JRSResume: require('./core/jrs-resume'), Theme: require('./core/theme'), FluentDate: require('./core/fluent-date'), HtmlGenerator: require('./gen/html-generator'), diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js index 4df7db7..3492f33 100644 --- a/tests/test-fresh-sheet.js +++ b/tests/test-fresh-sheet.js @@ -4,7 +4,7 @@ var chai = require('chai') , should = chai.should() , path = require('path') , _ = require('underscore') - , FreshSheet = require('../src/core/fresh-sheet') + , FRESHResume = require('../src/core/fresh-resume') , validator = require('is-my-json-valid'); chai.config.includeStack = false; @@ -15,7 +15,7 @@ describe('fullstack.json (FRESH)', function () { it('should open without throwing an exception', function () { function tryOpen() { - _sheet = new FreshSheet().open( 'tests/exemplars/fresh-exemplar.json' ); + _sheet = new FRESHResume().open( 'tests/exemplars/fresh-exemplar.json' ); } tryOpen.should.not.Throw(); }); @@ -45,7 +45,7 @@ describe('fullstack.json (FRESH)', function () { }); it('should not be modified after saving', function() { - var savedSheet = new FreshSheet().open( 'tests/sandbox/fullstack.json' ); + var savedSheet = new FRESHResume().open( 'tests/sandbox/fullstack.json' ); _sheet.stringify().should.equal( savedSheet.stringify() ) }); diff --git a/tests/test-jrs-sheet.js b/tests/test-jrs-sheet.js index aa40c95..5fccb35 100644 --- a/tests/test-jrs-sheet.js +++ b/tests/test-jrs-sheet.js @@ -4,7 +4,7 @@ var chai = require('chai') , should = chai.should() , path = require('path') , _ = require('underscore') - , Sheet = require('../src/core/sheet') + , JRSResume = require('../src/core/jrs-resume') , validator = require('is-my-json-valid'); chai.config.includeStack = false; @@ -15,7 +15,7 @@ describe('fullstack.json (JRS)', function () { it('should open without throwing an exception', function () { function tryOpen() { - _sheet = new Sheet().open( 'node_modules/resample/fullstack/in/resume.json' ); + _sheet = new JRSResume().open( 'node_modules/resample/fullstack/in/resume.json' ); } tryOpen.should.not.Throw(); }); @@ -44,7 +44,7 @@ describe('fullstack.json (JRS)', function () { }); it('should not be modified after saving', function() { - var savedSheet = new Sheet().open( 'tests/sandbox/fullstack.json' ); + var savedSheet = new JRSResume().open( 'tests/sandbox/fullstack.json' ); _sheet.stringify().should.equal( savedSheet.stringify() ) }); From 458c8519b5927cc26153825dc44fb6725801a89e Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 10:39:48 -0500 Subject: [PATCH 10/36] Update FRESH tests with recent changes. --- tests/test-fresh-sheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js index 3492f33..943826a 100644 --- a/tests/test-fresh-sheet.js +++ b/tests/test-fresh-sheet.js @@ -23,7 +23,7 @@ describe('fullstack.json (FRESH)', function () { it('should have one or more of each section', function() { expect( //(_sheet.basics) && - (_sheet.name && _sheet.label && _sheet.class && _sheet.summary) && + (_sheet.name && _sheet.info && _sheet.location) && (_sheet.employment.history && _sheet.employment.history.length > 0) && (_sheet.skills && _sheet.skills.length > 0) && (_sheet.education.history && _sheet.education.history.length > 0) && From 87618afa8dd483705d5a3b44ad3c947bb3aef5c1 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 10:39:59 -0500 Subject: [PATCH 11/36] Remove unused verb. --- src/fluentcmd.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fluentcmd.js b/src/fluentcmd.js index ebbfded..6de5ab8 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -181,8 +181,7 @@ module.exports = function () { return { verbs: { generate: gen, - validate: validate, - convert: convert + validate: validate }, lib: require('./fluentlib'), options: _opts, From 35b9f2b76443d42c9b6b77a5660618394f46dd15 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 12:36:58 -0500 Subject: [PATCH 12/36] Fix JSON date validation. JSON "date" type should accept YYYY, YYYY-MM, and YYYY-MM-DD but is-my-json-valid only validates the last of the three. --- src/core/fresh-resume.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index 137b1e4..ff5eb24 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -168,9 +168,10 @@ Definition of the FRESHResume class. */ FreshResume.prototype.isValid = function( info ) { var schemaObj = require('FRESCA'); - //var schemaObj = JSON.parse( schema ); var validator = require('is-my-json-valid') - var validate = validator( schemaObj ); + var validate = validator( schemaObj, { // Note [1] + formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } + }); var ret = validate( this ); if( !ret ) { this.meta = this.meta || { }; @@ -272,3 +273,9 @@ Definition of the FRESHResume class. 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}$/ +// From 0c1b1734ee997e494650715e0cc2776218858beb Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 15:39:26 -0500 Subject: [PATCH 13/36] Update tests. --- src/core/convert.js | 20 +++++++++++--------- tests/test-fresh-sheet.js | 16 ++++++++++------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/core/convert.js b/src/core/convert.js index 3f7a0e5..8745ee3 100644 --- a/src/core/convert.js +++ b/src/core/convert.js @@ -24,8 +24,8 @@ FRESH to JSON Resume conversion routiens. info: { label: jrs.basics.label, class: jrs.basics.label, - picture: jrs.basics.picture, - summary: jrs.basics.summary + image: jrs.basics.picture, + brief: jrs.basics.summary }, contact: { @@ -39,9 +39,7 @@ FRESH to JSON Resume conversion routiens. region: jrs.basics.location.region, country: jrs.basics.location.countryCode, code: jrs.basics.location.postalCode, - address: [ - jrs.basics.location.address, - ] + address: jrs.basics.location.address }, employment: { @@ -132,7 +130,9 @@ FRESH to JSON Resume conversion routiens. }; }), - interests: jrs.interests + interests: jrs.interests, + + references: jrs.references }; }, @@ -145,11 +145,11 @@ FRESH to JSON Resume conversion routiens. basics: { name: fresh.name, - summary: fresh.info.summary, + summary: fresh.info.brief, website: fresh.info.website, phone: fresh.info.phone, email: fresh.info.email, - picture: fresh.info.picture, + picture: fresh.info.image, location: { address: fresh.location.address.join('\n'), postalCode: fresh.location.code, @@ -233,7 +233,9 @@ FRESH to JSON Resume conversion routiens. }; }), - interests: fresh.interests + interests: fresh.interests, + + references: fresh.references }; diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js index 943826a..b8baff6 100644 --- a/tests/test-fresh-sheet.js +++ b/tests/test-fresh-sheet.js @@ -9,13 +9,14 @@ var chai = require('chai') chai.config.includeStack = false; -describe('fullstack.json (FRESH)', function () { +describe('fresh-resume-exemplar.json (FRESH)', function () { var _sheet; it('should open without throwing an exception', function () { function tryOpen() { - _sheet = new FRESHResume().open( 'tests/exemplars/fresh-exemplar.json' ); + _sheet = new FRESHResume().open( + 'node_modules/FRESCA/examples/fresh-resume-exemplar.json' ); } tryOpen.should.not.Throw(); }); @@ -23,13 +24,16 @@ describe('fullstack.json (FRESH)', function () { it('should have one or more of each section', function() { expect( //(_sheet.basics) && - (_sheet.name && _sheet.info && _sheet.location) && + _sheet.name && _sheet.info && _sheet.location && _sheet.contact && (_sheet.employment.history && _sheet.employment.history.length > 0) && (_sheet.skills && _sheet.skills.length > 0) && (_sheet.education.history && _sheet.education.history.length > 0) && (_sheet.service.history && _sheet.service.history.length > 0) && - (_sheet.publications && _sheet.publications.length > 0) //&& - //(_sheet.awards && _sheet.awards.length > 0) + (_sheet.publications && _sheet.publications.length > 0) && + (_sheet.recognition && _sheet.recognition.length > 0) && + (_sheet.samples && _sheet.samples.length > 0) && + (_sheet.references && _sheet.references.length > 0) && + (_sheet.interests && _sheet.interests.length > 0) ).to.equal( true ); }); @@ -45,7 +49,7 @@ describe('fullstack.json (FRESH)', function () { }); it('should not be modified after saving', function() { - var savedSheet = new FRESHResume().open( 'tests/sandbox/fullstack.json' ); + var savedSheet = new FRESHResume().open('tests/sandbox/fullstack.json'); _sheet.stringify().should.equal( savedSheet.stringify() ) }); From 1b3fdfbc5fe17a0567392b688f21f732cf1db9bd Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 16:24:56 -0500 Subject: [PATCH 14/36] Add FRESH validation test. --- tests/test-fresh-sheet.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js index b8baff6..a7a9aae 100644 --- a/tests/test-fresh-sheet.js +++ b/tests/test-fresh-sheet.js @@ -53,14 +53,15 @@ describe('fresh-resume-exemplar.json (FRESH)', function () { _sheet.stringify().should.equal( savedSheet.stringify() ) }); - // it('should validate against the FRESH resume schema', function() { - // var schemaJson = require('../src/core/resume.json'); - // var validate = validator( schemaJson, { verbose: true } ); - // var result = validate( JSON.parse( _sheet.meta.raw ) ); - // result || console.log("\n\nOops, resume didn't validate. " + - // "Validation errors:\n\n", validate.errors, "\n\n"); - // result.should.equal( true ); - // }); + it('should validate against the FRESH resume schema', function() { + var result = _sheet.isValid(); + // var schemaJson = require('FRESCA'); + // var validate = validator( schemaJson, { verbose: true } ); + // var result = validate( JSON.parse( _sheet.meta.raw ) ); + result || console.log("\n\nOops, resume didn't validate. " + + "Validation errors:\n\n", _sheet.meta.validationErrors, "\n\n"); + result.should.equal( true ); + }); }); From 15a74587bcdd9b0418b319f5eaec822e9915e7cf Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 17:19:27 -0500 Subject: [PATCH 15/36] Update FRESH tests with new exemplar name. --- tests/test-fresh-sheet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js index a7a9aae..406531f 100644 --- a/tests/test-fresh-sheet.js +++ b/tests/test-fresh-sheet.js @@ -9,14 +9,14 @@ var chai = require('chai') chai.config.includeStack = false; -describe('fresh-resume-exemplar.json (FRESH)', function () { +describe('jane-doe.json (FRESH)', function () { var _sheet; it('should open without throwing an exception', function () { function tryOpen() { _sheet = new FRESHResume().open( - 'node_modules/FRESCA/examples/fresh-resume-exemplar.json' ); + 'node_modules/FRESCA/exemplar/jane-doe.json' ); } tryOpen.should.not.Throw(); }); From c96d37b7f13456abf14e506eb20cdcd07fd99fc3 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Thu, 19 Nov 2015 17:43:54 -0500 Subject: [PATCH 16/36] Update README. --- README.md | 84 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ea2d501..238a506 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ fluentCV ======== *Generate beautiful, targeted resumes from your command line or shell.* -FluentCV is a **hackable, data-driven, dev-friendly resume authoring tool** with support for HTML, Markdown, Word, PDF, plain text, smoke signal, carrier pigeon, and other arbitrary-format resumes and CVs. +FluentCV is a **hackable, data-driven, dev-friendly resume authoring tool** with support for HTML, Markdown, Word, PDF, YAML, plain text, smoke signal, carrier pigeon, and other resume formats. ![](assets/fluentcv_cli_ubuntu.png) @@ -11,21 +11,22 @@ Looking for a desktop GUI version with pretty timelines and graphs? Check out [F ## Features - Runs on OS X, Linux, and Windows. -- Store your resume data as a durable, versionable JSON, YML, or XML document. -- Generate multiple targeted resumes in multiple formats, based on your needs. -- Output to HTML, PDF, Markdown, Word, JSON, YAML, XML, or a custom format. -- Never update one piece of information in four different resumes again. -- Compatible with the [JSON Resume standard][6] and [authoring tools][7]. +- Store your resume data as a durable, versionable JSON or YAML document. +- Generate polished resumes in multiple formats without violating DRY. +- Output to HTML, PDF, Markdown, MS Word, JSON, YAML, plain text, or XML. +- Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and [FCV Desktop][7]. +- Validate resumes against the FRESH or JSON Resume schema. +- Support for multiple input and output resumes. - Free and open-source through the MIT license. - Forthcoming: StackOverflow and LinkedIn support. -- Forthcoming: More themes! +- Forthcoming: More themes. ## Install FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then: -1. (Optional, for PDF support) Install the latest official [wkhtmltopdf][3] binary for your platform. -2. Install **fluentCV** by running `npm install fluentcv -g`. +1. Install the latest official [wkhtmltopdf][3] binary for your platform. +2. Install **fluentCV** with `[sudo] npm install fluentcv -g`. 3. You're ready to go. ## Use @@ -33,32 +34,32 @@ FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then: Assuming you've got a JSON-formatted resume handy, generating resumes in different formats and combinations easy. Just run: ```bash -fluentcv [inputs] [outputs] [-t theme]. +fluentcv generate [inputs] [outputs] [-t theme]. ``` Where `[inputs]` is one or more .json resume files, separated by spaces; `[outputs]` is one or more destination resumes, each prefaced with the `-o` option; and `[theme]` is the desired theme. For example: ```bash # Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.) -fluentcv resume.json -o out/resume.all -t modern +fluentcv generate resume.json -o out/resume.all -t modern # Generate a specific resume format -fluentcv resume.json -o out/resume.html -fluentcv resume.json -o out/resume.pdf -fluentcv resume.json -o out/resume.md -fluentcv resume.json -o out/resume.doc -fluentcv resume.json -o out/resume.json -fluentcv resume.json -o out/resume.txt -fluentcv resume.json -o out/resume.yml +fluentcv generate resume.json -o out/resume.html +fluentcv generate resume.json -o out/resume.pdf +fluentcv generate resume.json -o out/resume.md +fluentcv generate resume.json -o out/resume.doc +fluentcv generate resume.json -o out/resume.json +fluentcv generate resume.json -o out/resume.txt +fluentcv generate resume.json -o out/resume.yml # Specify 2 inputs and 3 outputs -fluentcv in1.json in2.json -o out.html -o out.doc -o out.pdf +fluentcv generate in1.json in2.json -o out.html -o out.doc -o out.pdf ``` You should see something to the effect of: ``` -*** FluentCV v0.7.2 *** +*** FluentCV v0.9.0 *** Reading JSON resume: foo/resume.json Applying MODERN Theme (7 formats) Generating HTML resume: out/resume.html @@ -77,11 +78,11 @@ Generating YAML resume: out/resume.yml You can specify a predefined or custom theme via the optional `-t` parameter. For a predefined theme, include the theme name. For a custom theme, include the path to the custom theme's folder. ```bash -fluentcv resume.json -t modern -fluentcv resume.json -t ~/foo/bar/my-custom-theme/ +fluentcv generate resume.json -t modern +fluentcv generate resume.json -t ~/foo/bar/my-custom-theme/ ``` -As of v0.7.2, available predefined themes are `modern`, `minimist`, and `hello-world`, and `compact`. +As of v0.9.0, available predefined themes are `modern`, `minimist`, and `hello-world`, and `compact`. ### Merging resumes @@ -89,13 +90,13 @@ You can **merge multiple resumes together** by specifying them in order from mos ```bash # Merge specific.json onto base.json and generate all formats -fluentcv base.json specific.json -o resume.all +fluentcv generate base.json specific.json -o resume.all ``` This can be useful for overriding a base (generic) resume with information from a specific (targeted) resume. For example, you might override your generic catch-all "software developer" resume with specific details from your targeted "game developer" resume, or combine two partial resumes into a "complete" resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge: ```bash -fluentcv in1.json in2.json in3.json in4.json -o out.html -o out.doc +fluentcv generate in1.json in2.json in3.json in4.json -o out.html -o out.doc Reading JSON resume: in1.json Reading JSON resume: in2.json Reading JSON resume: in3.json @@ -111,14 +112,14 @@ You can specify **multiple output targets** and FluentCV will build them: ```bash # Generate out1.doc, out1.pdf, and foo.txt from me.json. -fluentcv me.json -o out1.doc -o out1.pdf -o foo.txt +fluentcv generate me.json -o out1.doc -o out1.pdf -o foo.txt ``` You can also omit the output file(s) and/or theme completely: ```bash # Equivalent to "fluentcv resume.json resume.all -t modern" -fluentcv resume.json +fluentcv generate resume.json ``` ### Using .all @@ -127,17 +128,36 @@ The special `.all` extension tells FluentCV to generate all supported output for ```bash # Generate all resume formats (HTML, PDF, DOC, TXT, etc.) -fluentcv me.json -o out/resume.all +fluentcv generate me.json -o out/resume.all ``` ..tells FluentCV to read `me.json` and generate `out/resume.md`, `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`. +### Validating + +FluentCV can also validate your resumes against either the [FRESH / +FRESCA][fresca] or [JSON Resume][6] formats. To validate one or more existing +resumes, use the `validate` command: + +```bash +# Validate myresume.json against either the FRESH or JSON Resume schema. +fluentcv validate resumeA.json resumeB.json +``` + +FluentCV will validate each specified resume in turn: + +```bash +*** FluentCV v0.9.0 *** +Validating JSON resume: resume.json (INVALID) +Validating JSON resume: resume.json (VALID) +``` + ### Prettifying FluentCV applies [js-beautify][10]-style HTML prettification by default to HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag can be used: ```bash -fluentcv resume.json out.all --nopretty +fluentcv generate resume.json out.all --nopretty ``` ### Silent Mode @@ -145,8 +165,8 @@ fluentcv resume.json out.all --nopretty Use `-s` or `--silent` to run in silent mode: ```bash -fluentcv resume.json -o someFile.all -s -fluentcv resume.json -o someFile.all --silent +fluentcv generate resume.json -o someFile.all -s +fluentcv generate resume.json -o someFile.all --silent ``` ## License @@ -163,3 +183,5 @@ MIT. Go crazy. See [LICENSE.md][1] for details. [8]: https://youtu.be/N9wsjroVlu8 [9]: https://api.jquery.com/jquery.extend/ [10]: https://github.com/beautify-web/js-beautify +[fresh]: https://github.com/fluentdesk/FRESH +[fresca]: https://github.com/fluentdesk/FRESCA From 16cf97e08ea04d5978038a4276659097c2ad2cc2 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Fri, 20 Nov 2015 08:27:39 -0500 Subject: [PATCH 17/36] Improve converter. --- src/core/convert.js | 112 ++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 34 deletions(-) diff --git a/src/core/convert.js b/src/core/convert.js index 8745ee3..7e15cbf 100644 --- a/src/core/convert.js +++ b/src/core/convert.js @@ -23,7 +23,7 @@ FRESH to JSON Resume conversion routiens. info: { label: jrs.basics.label, - class: jrs.basics.label, + //class: jrs.basics.label, image: jrs.basics.picture, brief: jrs.basics.summary }, @@ -32,8 +32,23 @@ FRESH to JSON Resume conversion routiens. email: jrs.basics.email, phone: jrs.basics.phone, website: jrs.basics.website + //other: [none] }, + meta: jrs.meta, + + // disposition: { + // travel: 25, + // relocation: true, + // authorization: "citizen", + // commitment: ["full-time","permanent","contract"], + // remote: true, + // relocation: { + // willing: true, + // destinations: [ "Austin, TX", "California", "New York" ] + // } + // }, + location: { city: jrs.basics.location.city, region: jrs.basics.location.region, @@ -68,8 +83,8 @@ FRESH to JSON Resume conversion routiens. curriculum: edu.courses, url: edu.website || edu.url || null, summary: null, - // ???: edu.area, TODO - // ???: edu.studyType TODO + area: edu.area, + studyType: edu.studyType }; }) }, @@ -89,25 +104,18 @@ FRESH to JSON Resume conversion routiens. }) }, - skills: jrs.skills.map(function(sk){ - return { - name: sk.name, - summary: "", - level: sk.level, - summary: sk.keywords.join(', '), - years: null, - proof: null - }; - }), + skills: skillsToFRESH( jrs.skills ), - publications: jrs.publications.map(function(pub){ + writing: jrs.publications.map(function(pub){ return { title: pub.name, publisher: pub.publisher, link: [ { 'url': pub.website } ], - year: pub.releaseDate + year: pub.releaseDate, + date: pub.releaseDate, + summary: pub.summary }; }), @@ -132,7 +140,9 @@ FRESH to JSON Resume conversion routiens. interests: jrs.interests, - references: jrs.references + references: jrs.references, + + languages: jrs.languages }; }, @@ -145,13 +155,14 @@ FRESH to JSON Resume conversion routiens. basics: { name: fresh.name, + label: fresh.info.label, summary: fresh.info.brief, - website: fresh.info.website, - phone: fresh.info.phone, - email: fresh.info.email, + website: fresh.contact.website, + phone: fresh.contact.phone, + email: fresh.contact.email, picture: fresh.info.image, location: { - address: fresh.location.address.join('\n'), + address: fresh.location.address, postalCode: fresh.location.code, city: fresh.location.city, countryCode: fresh.location.country, @@ -169,6 +180,7 @@ FRESH to JSON Resume conversion routiens. work: fresh.employment.history.map(function(emp){ return { company: emp.employer, + website: emp.url, position: emp.position, startDate: emp.start, endDate: emp.end, @@ -184,20 +196,12 @@ FRESH to JSON Resume conversion routiens. courses: edu.curriculum, startDate: edu.start, endDate: edu.end, - area: "", // TODO - studyType: "" + area: edu.area, + studyType: edu.studyType }; }), - skills: fresh.skills.map( function(sk){ - return { - name: sk.name, - level: sk.level, - keywords: [], // TODO - //???: sk.years, - //???: sk.summary - }; - }), + skills: skillsToJRS( fresh.skills ), volunteer: fresh.service.history.map(function(srv){ return { @@ -223,10 +227,10 @@ FRESH to JSON Resume conversion routiens. }; }), - publications: fresh.publications.map(function(pub){ + publications: fresh.writing.map(function(pub){ return { name: pub.title, - publisher: "", // TODO + publisher: pub.publisher, releaseDate: pub.date, website: pub.link[0].url, summary: pub.summary @@ -235,7 +239,9 @@ FRESH to JSON Resume conversion routiens. interests: fresh.interests, - references: fresh.references + references: fresh.references, + + languages: fresh.languages }; @@ -243,4 +249,42 @@ FRESH to JSON Resume conversion routiens. }; + function skillsToFRESH( skills ) { + + return { + sets: skills.map(function(set) { + return { + name: set.name, + level: set.level, + skills: set.keywords + }; + }) + }; + } + + function skillsToJRS( skills ) { + var ret = []; + if( skills.sets && skills.sets.length ) { + ret = skills.sets.map(function(set){ + return { + name: set.name, + level: set.level, + keywords: set.skills + }; + }); + } + else if( skills.list ) { + ret = skills.list.map(function(sk){ + return { + name: sk.name, + level: sk.level, + keywords: sk.keywords + }; + }); + } + return ret; + } + + + }()); From c14176a5040ad4513e2137f18799d7a1af9bca6e Mon Sep 17 00:00:00 2001 From: devlinjd Date: Fri, 20 Nov 2015 08:29:19 -0500 Subject: [PATCH 18/36] Implement "convert" command. --- src/core/fresh-resume.js | 33 +++++- src/fluentcmd.js | 13 ++- tests/jrs-exemplar/richard-hendriks.json | 130 +++++++++++++++++++++++ tests/test-converter.js | 36 +++++++ 4 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 tests/jrs-exemplar/richard-hendriks.json create mode 100644 tests/test-converter.js diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index ff5eb24..3a30ff2 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -42,17 +42,40 @@ Definition of the FRESHResume class. }; /** - Convert this object to a JSON string, sanitizing meta-properties along the - way. Don't override .toString(). + Save the sheet to disk in a specific format, either FRESH or JSON Resume. */ - FreshResume.prototype.stringify = function() { + FreshResume.prototype.saveAs = function( filename, format ) { + this.meta.fileName = filename || this.meta.fileName; + if( format !== 'JRS' ) { + FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' ); + } + else { + var newRep = CONVERTER.toJRS( this ); + FS.writeFileSync( this.meta.fileName, FreshResume.stringify( newRep ), 'utf8' ); + } + return this; + } + + /** + Convert the supplied object to a JSON string, sanitizing meta-properties along + the way. + */ + FreshResume.stringify = function( obj ) { function replacer( key,value ) { // Exclude these keys from stringification return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'], function( val ) { return key.trim() === val; } ) ? undefined : value; } - return JSON.stringify( this, replacer, 2 ); + return JSON.stringify( obj, replacer, 2 ); + }, + + /** + Convert this object to a JSON string, sanitizing meta-properties along the + way. Don't override .toString(). + */ + FreshResume.prototype.stringify = function() { + return FreshResume.stringify( this ); }; /** @@ -66,7 +89,7 @@ Definition of the FRESHResume class. var rep = JSON.parse( stringData ); // Convert JSON Resume to FRESH if necessary - rep.basics && (rep = CONVERTER.toFRESH( rep )); + rep.basics && ( rep = CONVERTER.toFRESH( rep ) ); // Now apply the resume representation onto this object extend( true, this, rep ); diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 6de5ab8..b25b3ac 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -149,6 +149,16 @@ module.exports = function () { }); } + /** + Convert between FRESH and JRS formats. + */ + function convert( src, dst, opts, logger ) { + _log = logger || console.log; + if( !src || src.length !== 1 ) { throw { fluenterror: 3 }; } + var sheet = (new FLUENT.FRESHResume()).open( src[ 0 ] ); + sheet.saveAs( dst[0], sheet.meta.orgFormat === 'JRS' ? 'FRESH' : 'JRS' ); + } + /** Supported resume formats. */ @@ -181,7 +191,8 @@ module.exports = function () { return { verbs: { generate: gen, - validate: validate + validate: validate, + convert: convert }, lib: require('./fluentlib'), options: _opts, diff --git a/tests/jrs-exemplar/richard-hendriks.json b/tests/jrs-exemplar/richard-hendriks.json new file mode 100644 index 0000000..17fddad --- /dev/null +++ b/tests/jrs-exemplar/richard-hendriks.json @@ -0,0 +1,130 @@ +{ + "basics": { + "name": "Richard Hendriks", + "label": "Programmer", + "picture": "", + "email": "richard.hendriks@gmail.com", + "phone": "(912) 555-4321", + "website": "http://richardhendricks.com", + "summary": "Richard hails from Tulsa. He has earned degrees from the University of Oklahoma and Stanford. (Go Sooners and Cardinals!) Before starting Pied Piper, he worked for Hooli as a part time software developer. While his work focuses on applied information theory, mostly optimizing lossless compression schema of both the length-limited and adaptive variants, his non-work interests range widely, everything from quantum computing to chaos theory. He could tell you about it, but THAT would NOT be a “length-limited” conversation!", + "location": { + "address": "2712 Broadway St", + "postalCode": "CA 94115", + "city": "San Francisco", + "countryCode": "US", + "region": "California" + }, + "profiles": [ + { + "network": "Twitter", + "username": "neutralthoughts", + "url": "" + }, + { + "network": "SoundCloud", + "username": "dandymusicnl", + "url": "https://soundcloud.com/dandymusicnl" + } + ] + }, + "work": [ + { + "company": "Pied Piper", + "position": "CEO/President", + "website": "http://piedpiper.com", + "startDate": "2013-12-01", + "endDate": "2014-12-01", + "summary": "Pied Piper is a multi-platform technology based on a proprietary universal compression algorithm that has consistently fielded high Weisman Scores™ that are not merely competitive, but approach the theoretical limit of lossless compression.", + "highlights": [ + "Build an algorithm for artist to detect if their music was violating copy right infringement laws", + "Successfully won Techcrunch Disrupt", + "Optimized an algorithm that holds the current world record for Weisman Scores" + ] + } + ], + "volunteer": [ + { + "organization": "CoderDojo", + "position": "Teacher", + "website": "http://coderdojo.com/", + "startDate": "2012-01-01", + "endDate": "2013-01-01", + "summary": "Global movement of free coding clubs for young people.", + "highlights": [ + "Awarded 'Teacher of the Month'" + ] + } + ], + "education": [ + { + "institution": "University of Oklahoma", + "area": "Information Technology", + "studyType": "Bachelor", + "startDate": "2011-06-01", + "endDate": "2014-01-01", + "gpa": "4.0", + "courses": [ + "DB1101 - Basic SQL", + "CS2011 - Java Introduction" + ] + } + ], + "awards": [ + { + "title": "Digital Compression Pioneer Award", + "date": "2014-11-01", + "awarder": "Techcrunch", + "summary": "There is no spoon." + } + ], + "publications": [ + { + "name": "Video compression for 3d media", + "publisher": "Hooli", + "releaseDate": "2014-10-01", + "website": "http://en.wikipedia.org/wiki/Silicon_Valley_(TV_series)", + "summary": "Innovative middle-out compression algorithm that changes the way we store data." + } + ], + "skills": [ + { + "name": "Web Development", + "level": "Master", + "keywords": [ + "HTML", + "CSS", + "Javascript" + ] + }, + { + "name": "Compression", + "level": "Master", + "keywords": [ + "Mpeg", + "MP4", + "GIF" + ] + } + ], + "languages": [ + { + "language": "English", + "fluency": "Native speaker" + } + ], + "interests": [ + { + "name": "Wildlife", + "keywords": [ + "Ferrets", + "Unicorns" + ] + } + ], + "references": [ + { + "name": "Erlich Bachman", + "reference": "It is my pleasure to recommend Richard, his performance working as a consultant for Main St. Company proved that he will be a valuable addition to any company." + } + ] +} diff --git a/tests/test-converter.js b/tests/test-converter.js new file mode 100644 index 0000000..ac778e2 --- /dev/null +++ b/tests/test-converter.js @@ -0,0 +1,36 @@ + +var chai = require('chai') + , expect = chai.expect + , should = chai.should() + , path = require('path') + , _ = require('underscore') + , FRESHResume = require('../src/core/fresh-resume') + , CONVERTER = require('../src/core/convert') + , FS = require('fs') + , _ = require('underscore'); + +chai.config.includeStack = false; + +describe('FRESH/JRS converter', function () { + + var _sheet; + + it('should round-trip from JRS to FRESH to JRS without modifying or losing data', function () { + + var fileA = path.join( __dirname, 'jrs-exemplar/richard-hendriks.json' ); + var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' ); + + _sheet = new FRESHResume().open( fileA ); + _sheet.saveAs( fileB, 'JRS' ); + + var rawA = FS.readFileSync( fileA, 'utf8' ); + var rawB = FS.readFileSync( fileB, 'utf8' ); + + var objA = JSON.parse( rawA ); + var objB = JSON.parse( rawB ); + + _.isEqual(objA, objB).should.equal(true); + + }); + +}); From ad6d2c75ca85abafff3e679d3e750312dff4b084 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Fri, 20 Nov 2015 08:29:28 -0500 Subject: [PATCH 19/36] Update FRESH tests. --- tests/test-fresh-sheet.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js index 406531f..25b829d 100644 --- a/tests/test-fresh-sheet.js +++ b/tests/test-fresh-sheet.js @@ -26,10 +26,10 @@ describe('jane-doe.json (FRESH)', function () { //(_sheet.basics) && _sheet.name && _sheet.info && _sheet.location && _sheet.contact && (_sheet.employment.history && _sheet.employment.history.length > 0) && - (_sheet.skills && _sheet.skills.length > 0) && + (_sheet.skills && _sheet.skills.list.length > 0) && (_sheet.education.history && _sheet.education.history.length > 0) && (_sheet.service.history && _sheet.service.history.length > 0) && - (_sheet.publications && _sheet.publications.length > 0) && + (_sheet.writing && _sheet.writing.length > 0) && (_sheet.recognition && _sheet.recognition.length > 0) && (_sheet.samples && _sheet.samples.length > 0) && (_sheet.references && _sheet.references.length > 0) && @@ -43,13 +43,13 @@ describe('jane-doe.json (FRESH)', function () { it('should save without throwing an exception', function(){ function trySave() { - _sheet.save( 'tests/sandbox/fullstack.json' ); + _sheet.save( 'tests/sandbox/jane-doe.json' ); } trySave.should.not.Throw(); }); it('should not be modified after saving', function() { - var savedSheet = new FRESHResume().open('tests/sandbox/fullstack.json'); + var savedSheet = new FRESHResume().open('tests/sandbox/jane-doe.json'); _sheet.stringify().should.equal( savedSheet.stringify() ) }); From 9cde39703e3f8a50aad83accac01e3c49fd3977f Mon Sep 17 00:00:00 2001 From: devlinjd Date: Fri, 20 Nov 2015 09:28:55 -0500 Subject: [PATCH 20/36] Clean up handling of "meta". --- src/core/convert.js | 9 ++++++++- src/core/fresh-resume.js | 36 ++++++++++++++++++++---------------- src/core/jrs-resume.js | 18 +++++++++--------- src/fluentcmd.js | 8 ++++---- tests/test-fresh-sheet.js | 4 ++-- tests/test-jrs-sheet.js | 2 +- 6 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/core/convert.js b/src/core/convert.js index 7e15cbf..1c0170c 100644 --- a/src/core/convert.js +++ b/src/core/convert.js @@ -35,7 +35,7 @@ FRESH to JSON Resume conversion routiens. //other: [none] }, - meta: jrs.meta, + meta: meta2FRESH( jrs.meta ), // disposition: { // travel: 25, @@ -249,6 +249,13 @@ FRESH to JSON Resume conversion routiens. }; + function meta2FRESH( obj ) { + obj = obj || { }; + obj.format = obj.format || "FRESH@0.1.0"; + obj.version = obj.version || "0.1.0"; + return obj; + } + function skillsToFRESH( skills ) { return { diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index 3a30ff2..f5f702c 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -27,17 +27,17 @@ Definition of the FRESHResume class. consistent format. Then sort each section by startDate descending. */ FreshResume.prototype.open = function( file, title ) { - this.meta = { fileName: file }; - this.meta.raw = FS.readFileSync( file, 'utf8' ); - return this.parse( this.meta.raw, title ); + this.imp = { fileName: file }; + this.imp.raw = FS.readFileSync( file, 'utf8' ); + return this.parse( this.imp.raw, title ); }; /** Save the sheet to disk (for environments that have disk access). */ FreshResume.prototype.save = function( filename ) { - this.meta.fileName = filename || this.meta.fileName; - FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' ); + this.imp.fileName = filename || this.imp.fileName; + FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' ); return this; }; @@ -45,13 +45,13 @@ Definition of the FRESHResume class. Save the sheet to disk in a specific format, either FRESH or JSON Resume. */ FreshResume.prototype.saveAs = function( filename, format ) { - this.meta.fileName = filename || this.meta.fileName; + this.imp.fileName = filename || this.imp.fileName; if( format !== 'JRS' ) { - FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' ); + FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' ); } else { var newRep = CONVERTER.toJRS( this ); - FS.writeFileSync( this.meta.fileName, FreshResume.stringify( newRep ), 'utf8' ); + FS.writeFileSync( this.imp.fileName, FreshResume.stringify( newRep ), 'utf8' ); } return this; } @@ -62,7 +62,7 @@ Definition of the FRESHResume class. */ FreshResume.stringify = function( obj ) { function replacer( key,value ) { // Exclude these keys from stringification - return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', + return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'], function( val ) { return key.trim() === val; } ) ? undefined : value; @@ -89,16 +89,20 @@ Definition of the FRESHResume class. var rep = JSON.parse( stringData ); // Convert JSON Resume to FRESH if necessary - rep.basics && ( rep = CONVERTER.toFRESH( rep ) ); + if( rep.basics ) { + rep = CONVERTER.toFRESH( rep ); + rep.imp = rep.imp || { }; + rep.imp.orgFormat = 'JRS'; + } // Now apply the resume representation onto this object extend( true, this, rep ); // Set up metadata opts = opts || { }; - if( opts.meta === undefined || opts.meta ) { - this.meta = this.meta || { }; - this.meta.title = (opts.title || this.meta.title) || this.name; + if( opts.imp === undefined || opts.imp ) { + this.imp = this.imp || { }; + this.imp.title = (opts.title || this.imp.title) || this.name; } // Parse dates, sort dates, and calculate computed values (opts.date === undefined || opts.date) && _parseDates.call( this ); @@ -134,7 +138,7 @@ Definition of the FRESHResume class. */ FreshResume.prototype.clear = function( clearMeta ) { clearMeta = ((clearMeta === undefined) && true) || clearMeta; - clearMeta && (delete this.meta); + clearMeta && (delete this.imp); delete this.computed; // Don't use Object.keys() here delete this.employment; delete this.service; @@ -197,8 +201,8 @@ Definition of the FRESHResume class. }); var ret = validate( this ); if( !ret ) { - this.meta = this.meta || { }; - this.meta.validationErrors = validate.errors; + this.imp = this.imp || { }; + this.imp.validationErrors = validate.errors; } return ret; }; diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js index 55e8e29..b7fed04 100644 --- a/src/core/jrs-resume.js +++ b/src/core/jrs-resume.js @@ -30,17 +30,17 @@ Definition of the JRSResume class. consistent format. Then sort each section by startDate descending. */ JRSResume.prototype.open = function( file, title ) { - this.meta = { fileName: file }; - this.meta.raw = FS.readFileSync( file, 'utf8' ); - return this.parse( this.meta.raw, title ); + this.imp = { fileName: file }; + this.imp.raw = FS.readFileSync( file, 'utf8' ); + return this.parse( this.imp.raw, title ); }; /** Save the sheet to disk (for environments that have disk access). */ JRSResume.prototype.save = function( filename ) { - this.meta.fileName = filename || this.meta.fileName; - FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' ); + this.imp.fileName = filename || this.imp.fileName; + FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' ); return this; }; @@ -69,9 +69,9 @@ Definition of the JRSResume class. var rep = JSON.parse( stringData ); extend( true, this, rep ); // Set up metadata - if( opts.meta === undefined || opts.meta ) { - this.meta = this.meta || { }; - this.meta.title = (opts.title || this.meta.title) || this.basics.name; + if( opts.imp === undefined || opts.imp ) { + this.imp = this.imp || { }; + this.imp.title = (opts.title || this.imp.title) || this.basics.name; } // Parse dates, sort dates, and calculate computed values (opts.date === undefined || opts.date) && _parseDates.call( this ); @@ -110,7 +110,7 @@ Definition of the JRSResume class. */ JRSResume.prototype.clear = function( clearMeta ) { clearMeta = ((clearMeta === undefined) && true) || clearMeta; - clearMeta && (delete this.meta); + clearMeta && (delete this.imp); delete this.computed; // Don't use Object.keys() here delete this.work; delete this.volunteer; diff --git a/src/fluentcmd.js b/src/fluentcmd.js index b25b3ac..0bed242 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -43,8 +43,8 @@ module.exports = function () { // Merge input resumes... var msg = ''; rez = _.reduceRight( sheets, function( a, b, idx ) { - msg += ((idx == sheets.length - 2) ? 'Merging ' + a.meta.fileName : '') - + ' onto ' + b.meta.fileName; + msg += ((idx == sheets.length - 2) ? 'Merging ' + a.imp.fileName : '') + + ' onto ' + b.imp.fileName; return extend( true, b, a ); }); msg && _log(msg); @@ -144,7 +144,7 @@ module.exports = function () { _log( 'Validating JSON resume: ' + res + (valid ? ' (VALID)' : ' (INVALID)')); if( !valid ) { - _log( sheet.meta.validationErrors ); + _log( sheet.imp.validationErrors ); } }); } @@ -156,7 +156,7 @@ module.exports = function () { _log = logger || console.log; if( !src || src.length !== 1 ) { throw { fluenterror: 3 }; } var sheet = (new FLUENT.FRESHResume()).open( src[ 0 ] ); - sheet.saveAs( dst[0], sheet.meta.orgFormat === 'JRS' ? 'FRESH' : 'JRS' ); + sheet.saveAs( dst[0], sheet.imp.orgFormat === 'JRS' ? 'FRESH' : 'JRS' ); } /** diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js index 25b829d..d7aa2f5 100644 --- a/tests/test-fresh-sheet.js +++ b/tests/test-fresh-sheet.js @@ -57,9 +57,9 @@ describe('jane-doe.json (FRESH)', function () { var result = _sheet.isValid(); // var schemaJson = require('FRESCA'); // var validate = validator( schemaJson, { verbose: true } ); - // var result = validate( JSON.parse( _sheet.meta.raw ) ); + // var result = validate( JSON.parse( _sheet.imp.raw ) ); result || console.log("\n\nOops, resume didn't validate. " + - "Validation errors:\n\n", _sheet.meta.validationErrors, "\n\n"); + "Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n"); result.should.equal( true ); }); diff --git a/tests/test-jrs-sheet.js b/tests/test-jrs-sheet.js index 5fccb35..3afcba8 100644 --- a/tests/test-jrs-sheet.js +++ b/tests/test-jrs-sheet.js @@ -51,7 +51,7 @@ describe('fullstack.json (JRS)', function () { it('should validate against the JSON Resume schema', function() { var schemaJson = require('../src/core/resume.json'); var validate = validator( schemaJson, { verbose: true } ); - var result = validate( JSON.parse( _sheet.meta.raw ) ); + var result = validate( JSON.parse( _sheet.imp.raw ) ); result || console.log("\n\nOops, resume didn't validate. " + "Validation errors:\n\n", validate.errors, "\n\n"); result.should.equal( true ); From 4de997840e8861ad6157e80ff88beaf81b6f9102 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Fri, 20 Nov 2015 09:53:36 -0500 Subject: [PATCH 21/36] Scrub. --- src/fluentcmd.js | 74 ++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 0bed242..7e748d8 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -24,7 +24,7 @@ module.exports = function () { @param theme Friendly name of the resume theme. Defaults to "modern". @param logger Optional logging override. */ - function gen( src, dst, opts, logger, errHandler ) { + function generate( src, dst, opts, logger, errHandler ) { _log = logger || console.log; _err = errHandler || error; @@ -49,9 +49,9 @@ module.exports = function () { }); msg && _log(msg); - // Load the active theme // Verify the specified theme name/path - var tFolder = PATH.resolve( __dirname, '../node_modules/fluent-themes/themes', _opts.theme ); + var relativeThemeFolder = '../node_modules/fluent-themes/themes'; + var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme ); var exists = require('./utils/file-exists'); if (!exists( tFolder )) { tFolder = PATH.resolve( _opts.theme ); @@ -59,18 +59,27 @@ module.exports = function () { throw { fluenterror: 1, data: _opts.theme }; } } + + // Load the theme var theTheme = new FLUENT.Theme().open( tFolder ); _opts.themeObj = theTheme; - _log( 'Applying ' + theTheme.name.toUpperCase() + ' theme (' + Object.keys(theTheme.formats).length + ' formats)' ); + _log( 'Applying ' + theTheme.name.toUpperCase() + ' theme (' + + Object.keys(theTheme.formats).length + ' formats)' ); // Expand output resumes... (can't use map() here) - var targets = []; - var that = this; + var targets = [], that = this; ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { - var to = path.resolve(t), pa = path.parse(to), fmat = pa.ext || '.all'; + + var to = path.resolve(t), + pa = path.parse(to), + fmat = pa.ext || '.all'; + targets.push.apply(targets, fmat === '.all' ? - Object.keys( theTheme.formats ).map(function(k){ var z = theTheme.formats[k]; return { file: to.replace(/all$/g,z.pre), fmt: z } }) - : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); + Object.keys( theTheme.formats ).map(function(k){ + var z = theTheme.formats[k]; + return { file: to.replace(/all$/g,z.pre), fmt: z } + }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); + }); // Run the transformation! @@ -87,14 +96,16 @@ module.exports = function () { */ function single( fi, theme ) { try { - var f = fi.file, fType = fi.fmt.ext, fName = path.basename( f, '.' + fType ); + var f = fi.file, fType = fi.fmt.ext, fName = path.basename(f,'.'+fType); var fObj = _.property( fi.fmt.pre )( theme.formats ); - var fOut = path.join( f.substring( 0, f.lastIndexOf('.') + 1 ) + fObj.pre ); - _log( 'Generating ' + fi.fmt.title.toUpperCase() + ' resume: ' + path.relative(process.cwd(), f ) ); - var theFormat = _fmts.filter( function( fmt ) { - return fmt.name === fi.fmt.pre; - })[0]; - MKDIRP( path.dirname(fOut) ); // Ensure dest folder exists; don't bug user + var fOut = path.join( f.substring( 0, f.lastIndexOf('.')+1 ) + fObj.pre); + + _log( 'Generating ' + fi.fmt.title.toUpperCase() + ' resume: ' + + path.relative(process.cwd(), f ) ); + + var theFormat = _fmts.filter( + function( fmt ) { return fmt.name === fi.fmt.pre; })[0]; + MKDIRP( path.dirname(fOut) ); // Ensure dest folder exists; theFormat.gen.generate( rez, fOut, _opts ); } catch( ex ) { @@ -109,28 +120,6 @@ module.exports = function () { throw ex; } - /** - Validate 1 to N resumes as vanilla JSON. - */ - // function validateAsJSON( src, logger ) { - // _log = logger || console.log; - // if( !src || !src.length ) { throw { fluenterror: 3 }; } - // var isValid = true; - // var sheets = src.map( function( res ) { - // try { - // var rawJson = FS.readFileSync( res, 'utf8' ); - // var testObj = JSON.parse( rawJson ); - // } - // catch(ex) { - // if (!(ex instanceof SyntaxError)) { throw ex; } // [1] - // isValid = false; - // } - // - // _log( 'Validating JSON resume: ' + res + (isValid ? ' (VALID)' : ' (INVALID)')); - // return isValid; - // }); - // } - /** Validate 1 to N resumes in either FRESH or JSON Resume format. */ @@ -155,8 +144,13 @@ module.exports = function () { function convert( src, dst, opts, logger ) { _log = logger || console.log; if( !src || src.length !== 1 ) { throw { fluenterror: 3 }; } + _log( 'Reading JSON resume: ' + src[0] ); var sheet = (new FLUENT.FRESHResume()).open( src[ 0 ] ); - sheet.saveAs( dst[0], sheet.imp.orgFormat === 'JRS' ? 'FRESH' : 'JRS' ); + var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; + var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; + _log( 'Converting ' + src[0] + ' (' + sourceFormat + ') to ' + dst[0] + + ' (' + targetFormat + ').' ); + sheet.saveAs( dst[0], targetFormat ); } /** @@ -190,7 +184,7 @@ module.exports = function () { */ return { verbs: { - generate: gen, + generate: generate, validate: validate, convert: convert }, From 5304cbabd9eab12c5b209653701f0480df59d4b9 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Fri, 20 Nov 2015 15:29:38 -0500 Subject: [PATCH 22/36] Tweak converter. --- src/core/convert.js | 150 +++++++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 78 deletions(-) diff --git a/src/core/convert.js b/src/core/convert.js index 1c0170c..4ae1a95 100644 --- a/src/core/convert.js +++ b/src/core/convert.js @@ -15,55 +15,45 @@ FRESH to JSON Resume conversion routiens. /** Convert from JSON Resume format to FRESH. */ - toFRESH: function( jrs ) { + toFRESH: function( src, foreign ) { + + foreign = (foreign === undefined || foreign === null) ? true : foreign; return { - name: jrs.basics.name, + name: src.basics.name, info: { - label: jrs.basics.label, - //class: jrs.basics.label, - image: jrs.basics.picture, - brief: jrs.basics.summary + label: src.basics.label, + class: src.basics.class, // <--> round-trip + image: src.basics.picture, + brief: src.basics.summary }, contact: { - email: jrs.basics.email, - phone: jrs.basics.phone, - website: jrs.basics.website - //other: [none] + email: src.basics.email, + phone: src.basics.phone, + website: src.basics.website, + other: src.basics.other // <--> round-trip }, - meta: meta2FRESH( jrs.meta ), - - // disposition: { - // travel: 25, - // relocation: true, - // authorization: "citizen", - // commitment: ["full-time","permanent","contract"], - // remote: true, - // relocation: { - // willing: true, - // destinations: [ "Austin, TX", "California", "New York" ] - // } - // }, + meta: meta( true, src.meta ), location: { - city: jrs.basics.location.city, - region: jrs.basics.location.region, - country: jrs.basics.location.countryCode, - code: jrs.basics.location.postalCode, - address: jrs.basics.location.address + city: src.basics.location.city, + region: src.basics.location.region, + country: src.basics.location.countryCode, + code: src.basics.location.postalCode, + address: src.basics.location.address }, employment: { - history: jrs.work.map( function( job ) { + history: src.work.map( function( job ) { return { position: job.position, employer: job.company, summary: job.summary, - current: !job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current', + current: (!job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current') || undefined, start: job.startDate, end: job.endDate, url: job.website, @@ -74,7 +64,7 @@ FRESH to JSON Resume conversion routiens. }, education: { - history: jrs.education.map(function(edu){ + history: src.education.map(function(edu){ return { institution: edu.institution, start: edu.startDate, @@ -90,7 +80,7 @@ FRESH to JSON Resume conversion routiens. }, service: { - history: jrs.volunteer.map(function(vol) { + history: src.volunteer.map(function(vol) { return { type: 'volunteer', position: vol.position, @@ -104,22 +94,20 @@ FRESH to JSON Resume conversion routiens. }) }, - skills: skillsToFRESH( jrs.skills ), + skills: skillsToFRESH( src.skills ), - writing: jrs.publications.map(function(pub){ + writing: src.publications.map(function(pub){ return { title: pub.name, + flavor: undefined, publisher: pub.publisher, - link: [ - { 'url': pub.website } - ], - year: pub.releaseDate, + url: pub.website, date: pub.releaseDate, summary: pub.summary }; }), - recognition: jrs.awards.map(function(awd){ + recognition: src.awards.map(function(awd){ return { title: awd.title, date: awd.date, @@ -129,7 +117,7 @@ FRESH to JSON Resume conversion routiens. }; }), - social: jrs.basics.profiles.map(function(pro){ + social: src.basics.profiles.map(function(pro){ return { label: pro.network, network: pro.network, @@ -138,37 +126,41 @@ FRESH to JSON Resume conversion routiens. }; }), - interests: jrs.interests, - - references: jrs.references, - - languages: jrs.languages + interests: src.interests, + references: src.references, + languages: src.languages, + disposition: src.disposition // <--> round-trip }; }, /** Convert from FRESH format to JSON Resume. + @param foreign True if non-JSON-Resume properties should be included in + the result, false if those properties should be excluded. */ - toJRS: function( fresh ) { + toJRS: function( src, foreign ) { + + foreign = (foreign === undefined || foreign === null) ? false : foreign; return { basics: { - name: fresh.name, - label: fresh.info.label, - summary: fresh.info.brief, - website: fresh.contact.website, - phone: fresh.contact.phone, - email: fresh.contact.email, - picture: fresh.info.image, + name: src.name, + label: src.info.label, + class: foreign ? src.info.class : undefined, + summary: src.info.brief, + website: src.contact.website, + phone: src.contact.phone, + email: src.contact.email, + picture: src.info.image, location: { - address: fresh.location.address, - postalCode: fresh.location.code, - city: fresh.location.city, - countryCode: fresh.location.country, - region: fresh.location.region + address: src.location.address, + postalCode: src.location.code, + city: src.location.city, + countryCode: src.location.country, + region: src.location.region }, - profiles: fresh.social.map(function(soc){ + profiles: src.social.map(function(soc){ return { network: soc.network, username: soc.user, @@ -177,7 +169,7 @@ FRESH to JSON Resume conversion routiens. }) }, - work: fresh.employment.history.map(function(emp){ + work: src.employment.history.map(function(emp){ return { company: emp.employer, website: emp.url, @@ -189,7 +181,7 @@ FRESH to JSON Resume conversion routiens. }; }), - education: fresh.education.history.map(function(edu){ + education: src.education.history.map(function(edu){ return { institution: edu.institution, gpa: edu.grade, @@ -201,11 +193,11 @@ FRESH to JSON Resume conversion routiens. }; }), - skills: skillsToJRS( fresh.skills ), + skills: skillsToJRS( src.skills ), - volunteer: fresh.service.history.map(function(srv){ + volunteer: src.service.history.map(function(srv){ return { - //???: srv.type, + flavor: foreign ? srv.flavor : undefined, organization: srv.organization, position: srv.position, startDate: srv.start, @@ -216,10 +208,10 @@ FRESH to JSON Resume conversion routiens. }; }), - awards: fresh.recognition.map(function(awd){ + awards: src.recognition.map(function(awd){ return { - //???: awd.type, // TODO - //???: awd.url, + flavor: foreign ? awd.type : undefined, + url: foreign ? awd.url: undefined, title: awd.title, date: awd.date, awarder: awd.from, @@ -227,21 +219,21 @@ FRESH to JSON Resume conversion routiens. }; }), - publications: fresh.writing.map(function(pub){ + publications: src.writing.map(function(pub){ return { name: pub.title, publisher: pub.publisher, releaseDate: pub.date, - website: pub.link[0].url, + website: pub.url, summary: pub.summary }; }), - interests: fresh.interests, - - references: fresh.references, - - languages: fresh.languages + interests: src.interests, + references: src.references, + samples: foreign ? src.samples : undefined, + disposition: foreign ? src.disposition : undefined, + languages: src.languages }; @@ -249,10 +241,12 @@ FRESH to JSON Resume conversion routiens. }; - function meta2FRESH( obj ) { - obj = obj || { }; - obj.format = obj.format || "FRESH@0.1.0"; - obj.version = obj.version || "0.1.0"; + function meta( direction, obj ) { + if( direction ) { + obj = obj || { }; + obj.format = obj.format || "FRESH@0.1.0"; + obj.version = obj.version || "0.1.0"; + } return obj; } From bf34b01367985c96893d11a030abd46a66b97b70 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sat, 21 Nov 2015 03:10:11 -0500 Subject: [PATCH 23/36] Add YUIDoc support --- .gitignore | 1 + Gruntfile.js | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3d972f0..ce751fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ tests/sandbox/ +docs/ diff --git a/Gruntfile.js b/Gruntfile.js index 56169c7..90941b6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -15,15 +15,33 @@ module.exports = function (grunt) { reporter: 'spec' }, all: { src: ['tests/*.js'] } + }, + + yuidoc: { + compile: { + name: '<%= pkg.name %>', + description: '<%= pkg.description %>', + version: '<%= pkg.version %>', + url: '<%= pkg.homepage %>', + options: { + paths: 'src/', + //themedir: 'path/to/custom/theme/', + outdir: 'docs/' + } + } } }; grunt.initConfig( opts ); grunt.loadNpmTasks('grunt-simple-mocha'); - grunt.registerTask('test', 'Test the FluentLib library.', function( config ) { + grunt.loadNpmTasks('grunt-contrib-yuidoc'); + grunt.registerTask('test', 'Test the FluentCV library.', function( config ) { grunt.task.run( ['simplemocha:all'] ); }); - grunt.registerTask('default', [ 'test' ]); + grunt.registerTask('document', 'Generate FluentCV library documentation.', function( config ) { + grunt.task.run( ['yuidoc'] ); + }); + grunt.registerTask('default', [ 'test', 'yuidoc' ]); }; From debd8665459600a14c448a0689b06de1ddf30207 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sat, 21 Nov 2015 03:11:18 -0500 Subject: [PATCH 24/36] Adjust date references. --- src/core/convert.js | 2 +- src/core/fresh-resume.js | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/convert.js b/src/core/convert.js index 4ae1a95..f16aafc 100644 --- a/src/core/convert.js +++ b/src/core/convert.js @@ -210,7 +210,7 @@ FRESH to JSON Resume conversion routiens. awards: src.recognition.map(function(awd){ return { - flavor: foreign ? awd.type : undefined, + flavor: foreign ? awd.flavor : undefined, url: foreign ? awd.url: undefined, title: awd.title, date: awd.date, diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index f5f702c..fafe024 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -284,12 +284,14 @@ Definition of the FRESHResume class. end: _fmt( vol.end || 'current' ) }; }); - // this.awards && this.awards.forEach( function(awd) { - // awd.safeDate = _fmt( awd.date ); - // }); - this.publications && this.publications.forEach( function(pub) { + this.recognition && this.recognition.forEach( function(rec) { + rec.safe = { + date: _fmt( rec.date ) + }; + }); + this.writing && this.writing.forEach( function(pub) { pub.safe = { - date: _fmt( pub.year ) + date: _fmt( pub.date ) }; }); } From e44239b24a08cb5ca51f7bcd08038647fe3981a7 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sat, 21 Nov 2015 03:11:37 -0500 Subject: [PATCH 25/36] Update package.json deps. --- package.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bac13db..bd3d4f0 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,15 @@ "resume", "CV", "portfolio", - "Markdown" + "employment", + "career", + "Markdown", + "JSON", + "Word", + "PDF", + "YAML", + "HTML", + "CLI" ], "author": "James M. Devlin", "license": "MIT", @@ -24,6 +32,7 @@ }, "homepage": "https://github.com/fluentdesk/fluentcv", "dependencies": { + "FRESCA": "fluentdesk/FRESCA", "fluent-themes": "0.4.0-beta", "fs-extra": "^0.24.0", "html": "0.0.10", @@ -41,6 +50,7 @@ "devDependencies": { "chai": "*", "grunt": "*", + "grunt-contrib-yuidoc": "^0.10.0", "grunt-simple-mocha": "*", "is-my-json-valid": "^2.12.2", "mocha": "*", From 9fbab27d731a9a67f6870ea1112529af7bcabce4 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sat, 21 Nov 2015 05:56:16 -0500 Subject: [PATCH 26/36] Improve validation and color-coding. --- package.json | 1 + src/fluentcmd.js | 73 ++++++++++++++++++++++++++++++++++++++---------- src/index.js | 4 +-- 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index bd3d4f0..734de3e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "homepage": "https://github.com/fluentdesk/fluentcv", "dependencies": { "FRESCA": "fluentdesk/FRESCA", + "colors": "^1.1.2", "fluent-themes": "0.4.0-beta", "fs-extra": "^0.24.0", "html": "0.0.10", diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 7e748d8..09f7395 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -1,6 +1,7 @@ /** Internal resume generation logic for FluentCV. @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk +@module fluentcmd.js */ module.exports = function () { @@ -14,6 +15,7 @@ module.exports = function () { , FLUENT = require('./fluentlib') , PATH = require('path') , MKDIRP = require('mkdirp') + , COLORS = require('colors') , rez, _log, _err; /** @@ -36,15 +38,15 @@ module.exports = function () { // Load input resumes... if(!src || !src.length) { throw { fluenterror: 3 }; } var sheets = src.map( function( res ) { - _log( 'Reading JSON resume: ' + res ); + _log( 'Reading '.gray + 'SOURCE' + ' resume: '.gray + res.cyan.bold ); return (new FLUENT.FRESHResume()).open( res ); }); // Merge input resumes... var msg = ''; rez = _.reduceRight( sheets, function( a, b, idx ) { - msg += ((idx == sheets.length - 2) ? 'Merging ' + a.imp.fileName : '') - + ' onto ' + b.imp.fileName; + msg += ((idx == sheets.length - 2) ? 'Merging '.gray + a.imp.fileName : '') + + ' onto '.gray + b.imp.fileName; return extend( true, b, a ); }); msg && _log(msg); @@ -63,8 +65,8 @@ module.exports = function () { // Load the theme var theTheme = new FLUENT.Theme().open( tFolder ); _opts.themeObj = theTheme; - _log( 'Applying ' + theTheme.name.toUpperCase() + ' theme (' + - Object.keys(theTheme.formats).length + ' formats)' ); + _log( 'Applying '.yellow + theTheme.name.toUpperCase().yellow.bold + (' theme (' + + Object.keys(theTheme.formats).length + ' formats)').yellow ); // Expand output resumes... (can't use map() here) var targets = [], that = this; @@ -100,8 +102,8 @@ module.exports = function () { var fObj = _.property( fi.fmt.pre )( theme.formats ); var fOut = path.join( f.substring( 0, f.lastIndexOf('.')+1 ) + fObj.pre); - _log( 'Generating ' + fi.fmt.title.toUpperCase() + ' resume: ' + - path.relative(process.cwd(), f ) ); + _log( 'Generating '.green + fi.fmt.title.toUpperCase().green.bold + ' resume: '.green + + path.relative(process.cwd(), f ).green.bold ); var theFormat = _fmts.filter( function( fmt ) { return fmt.name === fi.fmt.pre; })[0]; @@ -127,14 +129,57 @@ module.exports = function () { _log = logger || console.log; if( !src || !src.length ) { throw { fluenterror: 3 }; } var isValid = true; - var sheets = src.map( function( res ) { - var sheet = (new FLUENT.FRESHResume()).open( res ); - var valid = sheet.isValid(); - _log( 'Validating JSON resume: ' + res + - (valid ? ' (VALID)' : ' (INVALID)')); - if( !valid ) { - _log( sheet.imp.validationErrors ); + + var validator = require('is-my-json-valid'); + var schemas = { + fresh: require('FRESCA'), + jars: require('./core/resume.json') + }; + + var sheets = src.map( function( file ) { + + var textData = ''; + try { + textData = FS.readFileSync( file, 'utf8' ); + var rez = JSON.parse( textData ); } + catch( ex ) { + _log('Validating ' + file.cyan.bold + ' against FRESH/JRS schema: ' + 'ERROR!'.red.bold); + + if (ex instanceof SyntaxError) { + // Invalid JSON + _log( '--> '.bold.red + file.toUpperCase().red + ' contains invalid JSON. Unable to validate.'.red ); + _log( (' INTERNAL: ' + ex).red ); + } + else { + + _log(('ERROR: ' + ex.toString()).red.bold); + } + return; + } + + var fmt = rez.meta && rez.meta.format === 'FRESH@0.1.0' ? 'fresh':'jars'; + process.stdout.write( 'Validating ' + file.cyan.bold + ' against ' + + fmt.replace('jars','JSON Resume').toUpperCase() + ' schema: ' ); + + var validate = validator( schemas[ fmt ], { // Note [1] + formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } + }); + + var ret = validate( rez ); + if( !ret ) { + rez.imp = rez.imp || { }; + rez.imp.validationErrors = validate.errors; + _log('INVALID'.bold.yellow); + rez.imp.validationErrors.forEach(function(err,idx){ + _log( '--> '.bold.yellow + ( err.field.replace('data.','resume.').toUpperCase() + + ' ' + err.message).yellow ); + }); + } + else { + _log('VALID!'.bold.green); + } + }); } diff --git a/src/index.js b/src/index.js index b528898..879eef5 100644 --- a/src/index.js +++ b/src/index.js @@ -24,7 +24,7 @@ catch( ex ) { function main() { // Setup - var title = '*** FluentCV v' + PKG.version + ' ***'; + var title = ('*** FluentCV v' + PKG.version + ' ***').bold; if( process.argv.length <= 2 ) { logMsg(title); throw { fluenterror: 4 }; } var a = ARGS( process.argv.slice(2) ); opts = getOpts( a ); @@ -78,7 +78,7 @@ function handleError( ex ) { var idx = msg.indexOf('Error: '); var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); - console.log( 'ERROR: ' + trimmed.toString() ); + console.log( ('ERROR: ' + trimmed.toString()).red.bold ); process.exit( exitCode ); } From 317de75a5b2be5ae3a61a8fe6429668c0bd546ee Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sat, 21 Nov 2015 07:59:30 -0500 Subject: [PATCH 27/36] Refactor. --- src/fluentcmd.js | 48 +++++++++++++++++++++++++++++++----------------- src/index.js | 23 +++++++++++++++-------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 09f7395..040b2d9 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -37,10 +37,7 @@ module.exports = function () { // Load input resumes... if(!src || !src.length) { throw { fluenterror: 3 }; } - var sheets = src.map( function( res ) { - _log( 'Reading '.gray + 'SOURCE' + ' resume: '.gray + res.cyan.bold ); - return (new FLUENT.FRESHResume()).open( res ); - }); + var sheets = loadSourceResumes( src ); // Merge input resumes... var msg = ''; @@ -136,19 +133,30 @@ module.exports = function () { jars: require('./core/resume.json') }; - var sheets = src.map( function( file ) { - - var textData = ''; + // Load input resumes... + var sheets = loadSourceResumes(src, function( res ) { try { - textData = FS.readFileSync( file, 'utf8' ); - var rez = JSON.parse( textData ); + return { + file: res, + raw: FS.readFileSync( res, 'utf8' ) + }; } catch( ex ) { - _log('Validating ' + file.cyan.bold + ' against FRESH/JRS schema: ' + 'ERROR!'.red.bold); + throw ex; + } + }); + + sheets.forEach( function( rep ) { + + try { + var rez = JSON.parse( rep.raw ); + } + catch( ex ) { + _log('Validating '.gray + rep.file.cyan.bold + ' against FRESH/JRS schema: '.gray + 'ERROR!'.red.bold); if (ex instanceof SyntaxError) { // Invalid JSON - _log( '--> '.bold.red + file.toUpperCase().red + ' contains invalid JSON. Unable to validate.'.red ); + _log( '--> '.bold.red + rep.file.toUpperCase().red + ' contains invalid JSON. Unable to validate.'.red ); _log( (' INTERNAL: ' + ex).red ); } else { @@ -159,8 +167,8 @@ module.exports = function () { } var fmt = rez.meta && rez.meta.format === 'FRESH@0.1.0' ? 'fresh':'jars'; - process.stdout.write( 'Validating ' + file.cyan.bold + ' against ' + - fmt.replace('jars','JSON Resume').toUpperCase() + ' schema: ' ); + process.stdout.write( 'Validating '.gray + rep.file + ' against '.gray + + fmt.replace('jars','JSON Resume').toUpperCase() + ' schema: '.gray ); var validate = validator( schemas[ fmt ], { // Note [1] formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } @@ -189,15 +197,21 @@ module.exports = function () { function convert( src, dst, opts, logger ) { _log = logger || console.log; if( !src || src.length !== 1 ) { throw { fluenterror: 3 }; } - _log( 'Reading JSON resume: ' + src[0] ); - var sheet = (new FLUENT.FRESHResume()).open( src[ 0 ] ); + var sheet = loadSourceResumes( src )[ 0 ]; var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; - _log( 'Converting ' + src[0] + ' (' + sourceFormat + ') to ' + dst[0] + - ' (' + targetFormat + ').' ); + _log( 'Converting '.gray + src[0] + (' (' + sourceFormat + ') to ').gray + dst[0] + + (' (' + targetFormat + ').').gray ); sheet.saveAs( dst[0], targetFormat ); } + function loadSourceResumes( src, fn ) { + return src.map( function( res ) { + _log( 'Reading '.gray + 'SOURCE' + ' resume: '.gray + res.cyan.bold ); + return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res ); + }); + } + /** Supported resume formats. */ diff --git a/src/index.js b/src/index.js index 879eef5..c151bfe 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,8 @@ Command-line interface (CLI) for FluentCV:CLI. var ARGS = require( 'minimist' ) , FCMD = require( './fluentcmd') , PKG = require('../package.json') - , opts = { }; + , opts = { } + , title = ('*** FluentCV v' + PKG.version + ' ***').white.bold; @@ -23,9 +24,7 @@ catch( ex ) { function main() { - // Setup - var title = ('*** FluentCV v' + PKG.version + ' ***').bold; - if( process.argv.length <= 2 ) { logMsg(title); throw { fluenterror: 4 }; } + if( process.argv.length <= 2 ) { throw { fluenterror: 4 }; } var a = ARGS( process.argv.slice(2) ); opts = getOpts( a ); logMsg( title ); @@ -33,7 +32,8 @@ function main() { // Get the action to be performed var verb = a._[0].toLowerCase().trim(); if( !FCMD.verbs[ verb ] ) { - throw 'Invalid command: "' + verb + '"'; + logMsg('Invalid command: "'.yellow + verb.yellow.bold + '"'.yellow); + return; } // Preload our params array @@ -66,8 +66,12 @@ function handleError( ex ) { switch( ex.fluenterror ) { // TODO: Remove magic numbers case 1: msg = "The specified theme couldn't be found: " + ex.data; break; case 2: msg = "Couldn't copy CSS file to destination folder"; break; - case 3: msg = "Please specify a valid JSON resume file."; break; - case 4: msg = "Please specify a valid command (GENERATE, VALIDATE, or CONVERT)." + case 3: msg = "Please specify a valid SOURCE resume in FRESH or JSON Resume format.".gray; break; + case 4: msg = title + "\nPlease specify a valid command (".gray + + Object.keys( FCMD.verbs ).map( function(v, idx, ar) { + return (idx === ar.length - 1 ? 'or '.gray : '') + + v.toUpperCase().white.bold; + }).join(', ') + ")"; }; exitCode = ex.fluenterror; } @@ -78,7 +82,10 @@ function handleError( ex ) { var idx = msg.indexOf('Error: '); var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); - console.log( ('ERROR: ' + trimmed.toString()).red.bold ); + if( !ex.fluenterror || ex.fluenterror !== 4 && ex.fluenterror !== 3 ) + console.log( ('ERROR: ' + trimmed.toString()).red.bold ); + else + console.log( trimmed.toString() ); process.exit( exitCode ); } From cbddb4b3aae50d9bb38c6d8eba42d9b5433e26b8 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sat, 21 Nov 2015 09:13:21 -0500 Subject: [PATCH 28/36] Add convenience filter for links. --- src/gen/template-generator.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 2e5e00f..dfc4521 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -31,7 +31,9 @@ var _defaultOpts = { xml: function( txt ) { return XML(txt); }, md: function( txt ) { return MD(txt); }, mdin: function( txt ) { return MD(txt).replace(/^\s*\|\<\/p\>\s*$/gi, ''); }, - lower: function( txt ) { return txt.toLowerCase(); } + lower: function( txt ) { return txt.toLowerCase(); }, + link: function( name, url ) { return url ? + '' + name + '' : name } }, prettify: { // ← See https://github.com/beautify-web/js-beautify#options indent_size: 2, From 992069b22d99f84a287ae42fb9bafc8c97a255cf Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sat, 21 Nov 2015 10:33:16 -0500 Subject: [PATCH 29/36] Cleanup. --- src/fluentcmd.js | 7 ++++--- src/gen/json-generator.js | 4 ++-- src/gen/template-generator.js | 1 - src/index.js | 33 ++++++++++++++++++++++++--------- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 040b2d9..f5ffe7f 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -104,7 +104,7 @@ module.exports = function () { var theFormat = _fmts.filter( function( fmt ) { return fmt.name === fi.fmt.pre; })[0]; - MKDIRP( path.dirname(fOut) ); // Ensure dest folder exists; + MKDIRP.sync( path.dirname(fOut) ); // Ensure dest folder exists; theFormat.gen.generate( rez, fOut, _opts ); } catch( ex ) { @@ -196,7 +196,8 @@ module.exports = function () { */ function convert( src, dst, opts, logger ) { _log = logger || console.log; - if( !src || src.length !== 1 ) { throw { fluenterror: 3 }; } + if( !src || !src.length ) { throw { fluenterror: 3 }; } + if( !dst || !dst.length ) { throw { fluenterror: 5 }; } var sheet = loadSourceResumes( src )[ 0 ]; var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; @@ -243,7 +244,7 @@ module.exports = function () { */ return { verbs: { - generate: generate, + build: generate, validate: validate, convert: convert }, diff --git a/src/gen/json-generator.js b/src/gen/json-generator.js index 218d45b..2099f41 100644 --- a/src/gen/json-generator.js +++ b/src/gen/json-generator.js @@ -19,9 +19,9 @@ var JsonGenerator = module.exports = BaseGenerator.extend({ invoke: function( rez ) { // TODO: merge with FCVD function replacer( key,value ) { // Exclude these keys from stringification - return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', + return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', - 'isModified', 'htmlPreview'], + 'isModified', 'htmlPreview', 'safe' ], function( val ) { return key.trim() === val; } ) ? undefined : value; } diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index dfc4521..d3b053a 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -127,7 +127,6 @@ var TemplateGenerator = module.exports = BaseGenerator.extend({ // Strip {# comments #} jst = jst.replace( _.templateSettings.comment, ''); - json.display_progress_bar = true; // Compile and run the template. TODO: avoid unnecessary recompiles. jst = _.template(jst)({ r: json, filt: this.opts.filters, cssInfo: cssInfo, headFragment: this.opts.headFragment || '' }); diff --git a/src/index.js b/src/index.js index c151bfe..82f6102 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,8 @@ var ARGS = require( 'minimist' ) , FCMD = require( './fluentcmd') , PKG = require('../package.json') , opts = { } - , title = ('*** FluentCV v' + PKG.version + ' ***').white.bold; + , title = ('*** FluentCV v' + PKG.version + ' ***').white.bold + , _ = require('underscore'); @@ -30,16 +31,28 @@ function main() { logMsg( title ); // Get the action to be performed - var verb = a._[0].toLowerCase().trim(); + var params = a._.map( function(p){ return p.toLowerCase().trim(); }); + var verb = params[0]; if( !FCMD.verbs[ verb ] ) { logMsg('Invalid command: "'.yellow + verb.yellow.bold + '"'.yellow); return; } + // Get source and dest params + var splitAt = _.indexOf( params, 'to' ); + if( splitAt === a._.length - 1 ) { + // 'TO' cannot be the last argument + logMsg('Please '.gray + 'specify an output file' + ' for this operation or '.gray + 'omit the TO keyword' + '.'.gray); + return; + } + + var src = a._.slice(1, splitAt === -1 ? undefined : splitAt ); + var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 ); + // Preload our params array - var dst = (a.o && ((typeof a.o === 'string' && [ a.o ]) || a.o)) || []; - dst = (dst === true) ? [] : dst; // Handle -o with missing output file - var parms = [ a._.slice(1) || [], dst, opts, logMsg ]; + //var dst = (a.o && ((typeof a.o === 'string' && [ a.o ]) || a.o)) || []; + //dst = (dst === true) ? [] : dst; // Handle -o with missing output file + var parms = [ src, dst, opts, logMsg ]; // Invoke the action FCMD.verbs[ verb ].apply( null, parms ); @@ -66,12 +79,14 @@ function handleError( ex ) { switch( ex.fluenterror ) { // TODO: Remove magic numbers case 1: msg = "The specified theme couldn't be found: " + ex.data; break; case 2: msg = "Couldn't copy CSS file to destination folder"; break; - case 3: msg = "Please specify a valid SOURCE resume in FRESH or JSON Resume format.".gray; break; - case 4: msg = title + "\nPlease specify a valid command (".gray + + case 3: msg = 'Please '.gray + 'specify a valid input resume' + ' in '.gray + 'FRESH' + ' or '.gray + 'JSON Resume' + ' format.'.gray; break; + case 4: msg = title + "\nPlease specify a command (".gray + Object.keys( FCMD.verbs ).map( function(v, idx, ar) { return (idx === ar.length - 1 ? 'or '.gray : '') - + v.toUpperCase().white.bold; + + v.toUpperCase(); }).join(', ') + ")"; + break; + case 5: msg = "Please specify the name of the TARGET file to convert to.".gray; }; exitCode = ex.fluenterror; } @@ -82,7 +97,7 @@ function handleError( ex ) { var idx = msg.indexOf('Error: '); var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); - if( !ex.fluenterror || ex.fluenterror !== 4 && ex.fluenterror !== 3 ) + if( !ex.fluenterror || ex.fluenterror < 3 ) console.log( ('ERROR: ' + trimmed.toString()).red.bold ); else console.log( trimmed.toString() ); From 5735ddc49528ca202e62f46b16ec4b00e587c6f8 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sat, 21 Nov 2015 16:12:22 -0500 Subject: [PATCH 30/36] Multiple enhancements. A set of rough enhancements supporting FRESH: - Added ability to process multiple sources for all commands (BUILD, VALIDATE, CONVERT). - Added new HELP command to show usage. - Improved error-handling and color-coding. --- src/fluentcmd.js | 99 +++++++++++++++++++++++++++++++----------------- src/index.js | 44 ++++++++++++++++----- src/use.txt | 22 +++++++++++ 3 files changed, 121 insertions(+), 44 deletions(-) create mode 100644 src/use.txt diff --git a/src/fluentcmd.js b/src/fluentcmd.js index f5ffe7f..295d12d 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -15,7 +15,7 @@ module.exports = function () { , FLUENT = require('./fluentlib') , PATH = require('path') , MKDIRP = require('mkdirp') - , COLORS = require('colors') + //, COLORS = require('colors') , rez, _log, _err; /** @@ -62,8 +62,8 @@ module.exports = function () { // Load the theme var theTheme = new FLUENT.Theme().open( tFolder ); _opts.themeObj = theTheme; - _log( 'Applying '.yellow + theTheme.name.toUpperCase().yellow.bold + (' theme (' + - Object.keys(theTheme.formats).length + ' formats)').yellow ); + _log( 'Applying '.status + theTheme.name.toUpperCase().infoBold + (' theme (' + + Object.keys(theTheme.formats).length + ' formats)').status ); // Expand output resumes... (can't use map() here) var targets = [], that = this; @@ -99,8 +99,8 @@ module.exports = function () { var fObj = _.property( fi.fmt.pre )( theme.formats ); var fOut = path.join( f.substring( 0, f.lastIndexOf('.')+1 ) + fObj.pre); - _log( 'Generating '.green + fi.fmt.title.toUpperCase().green.bold + ' resume: '.green + - path.relative(process.cwd(), f ).green.bold ); + _log( 'Generating '.useful + fi.fmt.title.toUpperCase().useful.bold + ' resume: '.useful + + path.relative(process.cwd(), f ).useful.bold ); var theFormat = _fmts.filter( function( fmt ) { return fmt.name === fi.fmt.pre; })[0]; @@ -124,7 +124,7 @@ module.exports = function () { */ function validate( src, unused, opts, logger ) { _log = logger || console.log; - if( !src || !src.length ) { throw { fluenterror: 3 }; } + if( !src || !src.length ) { throw { fluenterror: 6 }; } var isValid = true; var validator = require('is-my-json-valid'); @@ -152,7 +152,7 @@ module.exports = function () { var rez = JSON.parse( rep.raw ); } catch( ex ) { - _log('Validating '.gray + rep.file.cyan.bold + ' against FRESH/JRS schema: '.gray + 'ERROR!'.red.bold); + _log('Validating '.info + rep.file.infoBold + ' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold); if (ex instanceof SyntaxError) { // Invalid JSON @@ -166,27 +166,39 @@ module.exports = function () { return; } - var fmt = rez.meta && rez.meta.format === 'FRESH@0.1.0' ? 'fresh':'jars'; - process.stdout.write( 'Validating '.gray + rep.file + ' against '.gray + - fmt.replace('jars','JSON Resume').toUpperCase() + ' schema: '.gray ); + var isValid = false; + var style = 'useful'; + var errors = []; - var validate = validator( schemas[ fmt ], { // Note [1] - formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } + try { + + + + var fmt = rez.meta && rez.meta.format === 'FRESH@0.1.0' ? 'fresh':'jars'; + var validate = validator( schemas[ fmt ], { // Note [1] + formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } + }); + + isValid = validate( rez ); + if( !isValid ) { + style = 'warn'; + errors = validate.errors; + } + + } + catch(ex) { + + } + + _log( 'Validating '.info + rep.file.infoBold + ' against '.info + + fmt.replace('jars','JSON Resume').toUpperCase().infoBold + ' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold ); + + errors.forEach(function(err,idx){ + _log( '--> '.bold.yellow + ( err.field.replace('data.','resume.').toUpperCase() + + ' ' + err.message).yellow ); }); - var ret = validate( rez ); - if( !ret ) { - rez.imp = rez.imp || { }; - rez.imp.validationErrors = validate.errors; - _log('INVALID'.bold.yellow); - rez.imp.validationErrors.forEach(function(err,idx){ - _log( '--> '.bold.yellow + ( err.field.replace('data.','resume.').toUpperCase() - + ' ' + err.message).yellow ); - }); - } - else { - _log('VALID!'.bold.green); - } + }); } @@ -196,19 +208,35 @@ module.exports = function () { */ function convert( src, dst, opts, logger ) { _log = logger || console.log; - if( !src || !src.length ) { throw { fluenterror: 3 }; } - if( !dst || !dst.length ) { throw { fluenterror: 5 }; } - var sheet = loadSourceResumes( src )[ 0 ]; - var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; - var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; - _log( 'Converting '.gray + src[0] + (' (' + sourceFormat + ') to ').gray + dst[0] + - (' (' + targetFormat + ').').gray ); - sheet.saveAs( dst[0], targetFormat ); + if( !src || !src.length ) { throw { fluenterror: 6 }; } + if( !dst || !dst.length ) { + if( src.length === 1 ) { throw { fluenterror: 5 }; } + else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; } + else { throw { fluenterror: 5 }; } + } + if( src && dst && src.length && dst.length && src.length !== dst.length ) { + throw { fluenterror: 7 }; + } + var sheets = loadSourceResumes( src ); + sheets.forEach(function(sheet, idx){ + var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; + var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; + _log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' + sourceFormat + ') to ').useful + dst[0].useful.bold + + (' (' + targetFormat + ').').useful ); + sheet.saveAs( dst[idx], targetFormat ); + }); + } + + /** + Display help documentation. + */ + function help() { + console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).useful.bold ); } function loadSourceResumes( src, fn ) { return src.map( function( res ) { - _log( 'Reading '.gray + 'SOURCE' + ' resume: '.gray + res.cyan.bold ); + _log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.status + res.cyan.bold ); return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res ); }); } @@ -246,7 +274,8 @@ module.exports = function () { verbs: { build: generate, validate: validate, - convert: convert + convert: convert, + help: help }, lib: require('./fluentlib'), options: _opts, diff --git a/src/index.js b/src/index.js index 82f6102..bb4b5f9 100644 --- a/src/index.js +++ b/src/index.js @@ -8,8 +8,11 @@ Command-line interface (CLI) for FluentCV:CLI. var ARGS = require( 'minimist' ) , FCMD = require( './fluentcmd') , PKG = require('../package.json') + , COLORS = require('colors') + , FS = require('fs') + , PATH = require('path') , opts = { } - , title = ('*** FluentCV v' + PKG.version + ' ***').white.bold + , title = ('*** FluentCV v' + PKG.version + ' ***').bold.white , _ = require('underscore'); @@ -25,16 +28,30 @@ catch( ex ) { function main() { + // Colorize + COLORS.setTheme({ + title: ['white','bold'], + info: process.platform === 'win32' ? 'gray' : ['white','dim'], + infoBold: ['white','dim'], + warn: 'yellow', + error: 'red', + guide: 'yellow', + status: 'gray',//['white','dim'], + useful: 'green', + }); + + // Setup if( process.argv.length <= 2 ) { throw { fluenterror: 4 }; } var a = ARGS( process.argv.slice(2) ); opts = getOpts( a ); logMsg( title ); + // Get the action to be performed var params = a._.map( function(p){ return p.toLowerCase().trim(); }); var verb = params[0]; if( !FCMD.verbs[ verb ] ) { - logMsg('Invalid command: "'.yellow + verb.yellow.bold + '"'.yellow); + logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn); return; } @@ -42,7 +59,8 @@ function main() { var splitAt = _.indexOf( params, 'to' ); if( splitAt === a._.length - 1 ) { // 'TO' cannot be the last argument - logMsg('Please '.gray + 'specify an output file' + ' for this operation or '.gray + 'omit the TO keyword' + '.'.gray); + logMsg('Please '.warn + 'specify an output file'.warnBold + + ' for this operation or '.warn + 'omit the TO keyword'.warnBold + '.'.warn ); return; } @@ -75,20 +93,27 @@ function getOpts( args ) { function handleError( ex ) { var msg = '', exitCode; + + + if( ex.fluenterror ){ switch( ex.fluenterror ) { // TODO: Remove magic numbers case 1: msg = "The specified theme couldn't be found: " + ex.data; break; case 2: msg = "Couldn't copy CSS file to destination folder"; break; - case 3: msg = 'Please '.gray + 'specify a valid input resume' + ' in '.gray + 'FRESH' + ' or '.gray + 'JSON Resume' + ' format.'.gray; break; - case 4: msg = title + "\nPlease specify a command (".gray + + case 3: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in FRESH or JSON Resume format.'.guide; break; + case 4: msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide + Object.keys( FCMD.verbs ).map( function(v, idx, ar) { - return (idx === ar.length - 1 ? 'or '.gray : '') - + v.toUpperCase(); - }).join(', ') + ")"; + return (idx === ar.length - 1 ? 'or '.guide : '') + + v.toUpperCase().guide; + }).join(', '.guide) + ") to get started.\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; break; - case 5: msg = "Please specify the name of the TARGET file to convert to.".gray; + //case 4: msg = title + '\n' + ; break; + case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break; + case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break; + case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break; }; exitCode = ex.fluenterror; + } else { msg = ex.toString(); @@ -101,6 +126,7 @@ function handleError( ex ) { console.log( ('ERROR: ' + trimmed.toString()).red.bold ); else console.log( trimmed.toString() ); + process.exit( exitCode ); } diff --git a/src/use.txt b/src/use.txt new file mode 100644 index 0000000..1072347 --- /dev/null +++ b/src/use.txt @@ -0,0 +1,22 @@ +Usage: + + fluentcv [TO ] [-t ] + + should be BUILD, CONVERT, VALIDATE, or HELP. should +be the path to one or more FRESH or JSON Resume format resumes. +should be the name of the destination resume to be created, if any. The + parameter should be the name of a predefined theme (for example: +COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a +custom theme. + + fluentcv BUILD resume.json TO out/resume.all + fluentcv CONVERT resume.json TO resume-jrs.json + fluentcv VALIDATE resume.json + +Both SOURCES and TARGETS can accept multiple files: + + fluentCV BUILD r1.json r2.json TO out/resume.all out2/resume.html + fluentCV VALIDATE resume.json resume2.json resume3.json + +See https://github.com/fluentdesk/fluentCV/blob/master/README.md +for more information. From 0fe334f43328d43fb8b9293453b593805fad3463 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sat, 21 Nov 2015 23:28:33 -0500 Subject: [PATCH 31/36] Bump fluent-themes version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 734de3e..30d632c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "FRESCA": "fluentdesk/FRESCA", "colors": "^1.1.2", - "fluent-themes": "0.4.0-beta", + "fluent-themes": "0.5.0-beta", "fs-extra": "^0.24.0", "html": "0.0.10", "is-my-json-valid": "^2.12.2", From eade6f3a5c239ad833ba52e09293bbd6c937c59a Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 22 Nov 2015 00:07:30 -0500 Subject: [PATCH 32/36] Tweak colors. --- src/fluentcmd.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 295d12d..0a5c76e 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -216,7 +216,7 @@ module.exports = function () { } if( src && dst && src.length && dst.length && src.length !== dst.length ) { throw { fluenterror: 7 }; - } + } var sheets = loadSourceResumes( src ); sheets.forEach(function(sheet, idx){ var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; @@ -236,7 +236,7 @@ module.exports = function () { function loadSourceResumes( src, fn ) { return src.map( function( res ) { - _log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.status + res.cyan.bold ); + _log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info + res.cyan.bold ); return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res ); }); } From 42770989bc75e64176a47ceeefe2037accc518e1 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 22 Nov 2015 00:10:08 -0500 Subject: [PATCH 33/36] Tweak colors for Linux. --- src/fluentcmd.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 0a5c76e..2a329ba 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -62,8 +62,8 @@ module.exports = function () { // Load the theme var theTheme = new FLUENT.Theme().open( tFolder ); _opts.themeObj = theTheme; - _log( 'Applying '.status + theTheme.name.toUpperCase().infoBold + (' theme (' + - Object.keys(theTheme.formats).length + ' formats)').status ); + _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + (' theme (' + + Object.keys(theTheme.formats).length + ' formats)').info ); // Expand output resumes... (can't use map() here) var targets = [], that = this; From 5899989feb685651030a4b15b427cd2138cdadc6 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 22 Nov 2015 02:37:14 -0500 Subject: [PATCH 34/36] Update README. --- README.md | 142 ++++++++++++++++++++++++++------- assets/fluentcv_cli_ubuntu.png | Bin 74389 -> 53905 bytes 2 files changed, 114 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 238a506..2b113bd 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,90 @@ fluentCV ======== *Generate beautiful, targeted resumes from your command line or shell.* -FluentCV is a **hackable, data-driven, dev-friendly resume authoring tool** with support for HTML, Markdown, Word, PDF, YAML, plain text, smoke signal, carrier pigeon, and other resume formats. - ![](assets/fluentcv_cli_ubuntu.png) -Looking for a desktop GUI version with pretty timelines and graphs? Check out [FluentCV Desktop][7]. +FluentCV is a Swiss Army knife for resumes and CVs. Use it to: + +1. **Generate** polished multiformat resumes in HTML, Word, Markdown, PDF, plain +text, JSON, and YAML formats—without violating DRY. +2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats. +3. **Validate** resumes against either format. + +Install it with NPM: + +```bash +[sudo] npm install fluentcv -g +``` + +Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for +your platform. ## Features - Runs on OS X, Linux, and Windows. - Store your resume data as a durable, versionable JSON or YAML document. -- Generate polished resumes in multiple formats without violating DRY. +- Generate polished resumes in multiple formats without violating [DRY][dry]. - Output to HTML, PDF, Markdown, MS Word, JSON, YAML, plain text, or XML. -- Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and [FCV Desktop][7]. +- Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and +[FCV Desktop][7]. - Validate resumes against the FRESH or JSON Resume schema. - Support for multiple input and output resumes. - Free and open-source through the MIT license. - Forthcoming: StackOverflow and LinkedIn support. -- Forthcoming: More themes. +- Forthcoming: More commands and themes. + +Looking for a desktop GUI version for Windows / OS X / Linux? Check out +[FluentCV Desktop][7]. + +## Getting Started + +To use FluentCV you'll need to create a valid resume in either [FRESH][fresca] +or [JSON Resume][6] format. Then you can start using the command line tool. +There are three commands you should be aware of: + +- `build` generates resumes in HTML, Word, Markdown, PDF, and other formats. Use +it when you need to submit, upload, print, or email resumes in specific formats. + + ```bash + # fluentcv BUILD TO [-t THEME] + fluentcv BUILD resume.json TO out/resume.all + fluentcv BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all + ``` + +- `convert` converts your source resume between FRESH and JSON Resume formats. +Use it to convert between the two formats to take advantage of tools and services. + + ```bash + # fluentcv CONVERT TO + fluentcv CONVERT resume.json TO resume-jrs.json + fluentcv CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json + ``` + +- `validate` validates the specified resume against either the FRESH or JSON +Resume schema. Use it to make sure your resume data is sufficient and complete. + + ```bash + # fluentcv VALIDATE + fluentcv VALIDATE resume.json + fluentcv VALIDATE r1.json r2.json r3.json + ``` + +## Supported Output Formats + +FluentCV supports these output formats: + +Output Format | Ext | Notes +------------- | - | ------------- +HTML | .html | A standard HTML 5 + CSS resume format that can be viewed in a browser, deployed to a website, etc. +Markdown | .md | A structured Markdown document that can be used as-is or used to generate HTML. +MS Word | .doc | A Microsoft Word office document in 2003 format for widest compatibility. +Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme. +plain text | .txt | A formatted plain text document appropriate for emails or as a source for copy-paste. +JSON | .json | A JSON representation of the resume. Can be passed back into FluentCV as an input. +YAML | .yml | A YAML representation of the resume. Can be passed back into FluentCV as an input. +RTF | .rtf | Forthcoming. +Textile | .textile | Forthcoming. +image | .png, .bmp | Forthcoming. ## Install @@ -31,29 +97,32 @@ FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then: ## Use -Assuming you've got a JSON-formatted resume handy, generating resumes in different formats and combinations easy. Just run: +Assuming you've got a JSON-formatted resume handy, generating resumes in +different formats and combinations easy. Just run: ```bash -fluentcv generate [inputs] [outputs] [-t theme]. +fluentcv BUILD [-t theme]. ``` -Where `[inputs]` is one or more .json resume files, separated by spaces; `[outputs]` is one or more destination resumes, each prefaced with the `-o` option; and `[theme]` is the desired theme. For example: +Where `` is one or more .json resume files, separated by spaces; +`` is one or more destination resumes, and `` is the desired +theme (default to Modern). For example: ```bash # Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.) -fluentcv generate resume.json -o out/resume.all -t modern +fluentcv build resume.json -o out/resume.all -t modern # Generate a specific resume format -fluentcv generate resume.json -o out/resume.html -fluentcv generate resume.json -o out/resume.pdf -fluentcv generate resume.json -o out/resume.md -fluentcv generate resume.json -o out/resume.doc -fluentcv generate resume.json -o out/resume.json -fluentcv generate resume.json -o out/resume.txt -fluentcv generate resume.json -o out/resume.yml +fluentcv build resume.json TO out/resume.html +fluentcv build resume.json TO out/resume.pdf +fluentcv build resume.json TO out/resume.md +fluentcv build resume.json TO out/resume.doc +fluentcv build resume.json TO out/resume.json +fluentcv build resume.json TO out/resume.txt +fluentcv build resume.json TO out/resume.yml # Specify 2 inputs and 3 outputs -fluentcv generate in1.json in2.json -o out.html -o out.doc -o out.pdf +fluentcv build in1.json in2.json TO out.html out.doc out.pdf ``` You should see something to the effect of: @@ -78,8 +147,8 @@ Generating YAML resume: out/resume.yml You can specify a predefined or custom theme via the optional `-t` parameter. For a predefined theme, include the theme name. For a custom theme, include the path to the custom theme's folder. ```bash -fluentcv generate resume.json -t modern -fluentcv generate resume.json -t ~/foo/bar/my-custom-theme/ +fluentcv build resume.json -t modern +fluentcv build resume.json -t ~/foo/bar/my-custom-theme/ ``` As of v0.9.0, available predefined themes are `modern`, `minimist`, and `hello-world`, and `compact`. @@ -90,13 +159,13 @@ You can **merge multiple resumes together** by specifying them in order from mos ```bash # Merge specific.json onto base.json and generate all formats -fluentcv generate base.json specific.json -o resume.all +fluentcv build base.json specific.json -o resume.all ``` This can be useful for overriding a base (generic) resume with information from a specific (targeted) resume. For example, you might override your generic catch-all "software developer" resume with specific details from your targeted "game developer" resume, or combine two partial resumes into a "complete" resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge: ```bash -fluentcv generate in1.json in2.json in3.json in4.json -o out.html -o out.doc +fluentcv build in1.json in2.json in3.json in4.json TO out.html out.doc Reading JSON resume: in1.json Reading JSON resume: in2.json Reading JSON resume: in3.json @@ -112,14 +181,14 @@ You can specify **multiple output targets** and FluentCV will build them: ```bash # Generate out1.doc, out1.pdf, and foo.txt from me.json. -fluentcv generate me.json -o out1.doc -o out1.pdf -o foo.txt +fluentcv build me.json -o out1.doc -o out1.pdf -o foo.txt ``` You can also omit the output file(s) and/or theme completely: ```bash # Equivalent to "fluentcv resume.json resume.all -t modern" -fluentcv generate resume.json +fluentcv build resume.json ``` ### Using .all @@ -128,7 +197,7 @@ The special `.all` extension tells FluentCV to generate all supported output for ```bash # Generate all resume formats (HTML, PDF, DOC, TXT, etc.) -fluentcv generate me.json -o out/resume.all +fluentcv build me.json -o out/resume.all ``` ..tells FluentCV to read `me.json` and generate `out/resume.md`, `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`. @@ -148,13 +217,29 @@ FluentCV will validate each specified resume in turn: ```bash *** FluentCV v0.9.0 *** -Validating JSON resume: resume.json (INVALID) -Validating JSON resume: resume.json (VALID) +Validating JSON resume: resumeA.json (INVALID) +Validating JSON resume: resumeB.json (VALID) ``` +### Converting + +FluentCV can convert between the [FRESH][fresca] and [JSON Resume][6] formats. +Just run: + +```bash +fluentcv CONVERT +``` + +where is one or more resumes in FRESH or JSON Resume format, and + is a corresponding list of output file names. FluentCV will autodetect +the format (FRESH or JRS) of each input resume and convert it to the other +format (JRS or FRESH). + ### Prettifying -FluentCV applies [js-beautify][10]-style HTML prettification by default to HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag can be used: +FluentCV applies [js-beautify][10]-style HTML prettification by default to +HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag +can be used: ```bash fluentcv generate resume.json out.all --nopretty @@ -185,3 +270,4 @@ MIT. Go crazy. See [LICENSE.md][1] for details. [10]: https://github.com/beautify-web/js-beautify [fresh]: https://github.com/fluentdesk/FRESH [fresca]: https://github.com/fluentdesk/FRESCA +[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself diff --git a/assets/fluentcv_cli_ubuntu.png b/assets/fluentcv_cli_ubuntu.png index 338af470ecb1b65fe2e3b2f0d70a5f9e36055a1e..f4db94cd4ae883ec56eabcdc87ef9f0b71949a87 100644 GIT binary patch literal 53905 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%0AxALP{BJ0X{1fPvyVlh{SUMu?wsuRjNL*O&dR^S@ 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�Vw)2HMc` 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 literal 74389 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 z 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%0D7y}WcuVYD?9ovkSuCn#*1n1;C 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 Date: Sun, 22 Nov 2015 02:41:21 -0500 Subject: [PATCH 35/36] Fix README table glitch. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2b113bd..704c16e 100644 --- a/README.md +++ b/README.md @@ -74,15 +74,15 @@ Resume schema. Use it to make sure your resume data is sufficient and complete. FluentCV supports these output formats: -Output Format | Ext | Notes -------------- | - | ------------- +Output Format | Ext | Notes +------------- | --- | ----- HTML | .html | A standard HTML 5 + CSS resume format that can be viewed in a browser, deployed to a website, etc. Markdown | .md | A structured Markdown document that can be used as-is or used to generate HTML. -MS Word | .doc | A Microsoft Word office document in 2003 format for widest compatibility. +MS Word | .doc | A Microsoft Word office document. Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme. -plain text | .txt | A formatted plain text document appropriate for emails or as a source for copy-paste. -JSON | .json | A JSON representation of the resume. Can be passed back into FluentCV as an input. -YAML | .yml | A YAML representation of the resume. Can be passed back into FluentCV as an input. +plain text | .txt | A formatted plain text document appropriate for emails or copy-paste. +JSON | .json | A JSON representation of the resume. +YAML | .yml | A YAML representation of the resume. RTF | .rtf | Forthcoming. Textile | .textile | Forthcoming. image | .png, .bmp | Forthcoming. From 37225aec84c42e06d5023fdf8298bce7a59e31d4 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 22 Nov 2015 03:03:16 -0500 Subject: [PATCH 36/36] Update FRESCA version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 30d632c..cb0287a 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "homepage": "https://github.com/fluentdesk/fluentcv", "dependencies": { - "FRESCA": "fluentdesk/FRESCA", + "FRESCA": "fluentdesk/FRESCA#v0.1.0", "colors": "^1.1.2", "fluent-themes": "0.5.0-beta", "fs-extra": "^0.24.0",