1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-07-02 16:30:04 +01:00

Merge pull request #18 from fluentdesk/feat/fresh

Feat/fresh
This commit is contained in:
JD 2015-11-22 03:09:31 -05:00
commit 80315f12ac
19 changed files with 1329 additions and 128 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules/
tests/sandbox/
docs/

View File

@ -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' ]);
};

182
README.md
View File

@ -2,63 +2,133 @@ 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.
![](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&mdash;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, 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][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 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 <INPUTS> TO <OUTPUTS> [-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 <INPUTS> TO <OUTPUTS>
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 <INPUTS>
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.
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 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.
## 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
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 [inputs] [outputs] [-t theme].
fluentcv BUILD <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:
Where `<INPUTS>` is one or more .json resume files, separated by spaces;
`<OUTPUTS>` is one or more destination resumes, and `<THEME>` is the desired
theme (default to Modern). For example:
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.)
fluentcv resume.json -o out/resume.all -t modern
fluentcv build 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 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 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:
```
*** 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 +147,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 build resume.json -t modern
fluentcv build 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 +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 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 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
@ -111,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 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 resume.json
fluentcv build resume.json
```
### Using .all
@ -127,17 +197,52 @@ 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 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`.
### Prettifying
### Validating
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 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
fluentcv resume.json out.all --nopretty
# 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: 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 <INPUTS> <OUTPUTS>
```
where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and
<OUTPUTS> 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:
```bash
fluentcv generate resume.json out.all --nopretty
```
### Silent Mode
@ -145,8 +250,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 +268,6 @@ 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
[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -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",
@ -10,7 +10,15 @@
"resume",
"CV",
"portfolio",
"Markdown"
"employment",
"career",
"Markdown",
"JSON",
"Word",
"PDF",
"YAML",
"HTML",
"CLI"
],
"author": "James M. Devlin",
"license": "MIT",
@ -24,7 +32,9 @@
},
"homepage": "https://github.com/fluentdesk/fluentcv",
"dependencies": {
"fluent-themes": "0.4.0-beta",
"FRESCA": "fluentdesk/FRESCA#v0.1.0",
"colors": "^1.1.2",
"fluent-themes": "0.5.0-beta",
"fs-extra": "^0.24.0",
"html": "0.0.10",
"is-my-json-valid": "^2.12.2",
@ -41,6 +51,7 @@
"devDependencies": {
"chai": "*",
"grunt": "*",
"grunt-contrib-yuidoc": "^0.10.0",
"grunt-simple-mocha": "*",
"is-my-json-valid": "^2.12.2",
"mocha": "*",

291
src/core/convert.js Normal file
View File

@ -0,0 +1,291 @@
/**
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( src, foreign ) {
foreign = (foreign === undefined || foreign === null) ? true : foreign;
return {
name: src.basics.name,
info: {
label: src.basics.label,
class: src.basics.class, // <--> round-trip
image: src.basics.picture,
brief: src.basics.summary
},
contact: {
email: src.basics.email,
phone: src.basics.phone,
website: src.basics.website,
other: src.basics.other // <--> round-trip
},
meta: meta( true, src.meta ),
location: {
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: 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') || undefined,
start: job.startDate,
end: job.endDate,
url: job.website,
keywords: "",
highlights: job.highlights
};
})
},
education: {
history: src.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,
area: edu.area,
studyType: edu.studyType
};
})
},
service: {
history: src.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: skillsToFRESH( src.skills ),
writing: src.publications.map(function(pub){
return {
title: pub.name,
flavor: undefined,
publisher: pub.publisher,
url: pub.website,
date: pub.releaseDate,
summary: pub.summary
};
}),
recognition: src.awards.map(function(awd){
return {
title: awd.title,
date: awd.date,
summary: awd.summary,
from: awd.awarder,
url: null
};
}),
social: src.basics.profiles.map(function(pro){
return {
label: pro.network,
network: pro.network,
url: pro.url,
user: pro.username
};
}),
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( src, foreign ) {
foreign = (foreign === undefined || foreign === null) ? false : foreign;
return {
basics: {
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: src.location.address,
postalCode: src.location.code,
city: src.location.city,
countryCode: src.location.country,
region: src.location.region
},
profiles: src.social.map(function(soc){
return {
network: soc.network,
username: soc.user,
url: soc.url
};
})
},
work: src.employment.history.map(function(emp){
return {
company: emp.employer,
website: emp.url,
position: emp.position,
startDate: emp.start,
endDate: emp.end,
summary: emp.summary,
highlights: emp.highlights
};
}),
education: src.education.history.map(function(edu){
return {
institution: edu.institution,
gpa: edu.grade,
courses: edu.curriculum,
startDate: edu.start,
endDate: edu.end,
area: edu.area,
studyType: edu.studyType
};
}),
skills: skillsToJRS( src.skills ),
volunteer: src.service.history.map(function(srv){
return {
flavor: foreign ? srv.flavor : undefined,
organization: srv.organization,
position: srv.position,
startDate: srv.start,
endDate: srv.end,
website: srv.url,
summary: srv.summary,
highlights: srv.highlights
};
}),
awards: src.recognition.map(function(awd){
return {
flavor: foreign ? awd.flavor : undefined,
url: foreign ? awd.url: undefined,
title: awd.title,
date: awd.date,
awarder: awd.from,
summary: awd.summary
};
}),
publications: src.writing.map(function(pub){
return {
name: pub.title,
publisher: pub.publisher,
releaseDate: pub.date,
website: pub.url,
summary: pub.summary
};
}),
interests: src.interests,
references: src.references,
samples: foreign ? src.samples : undefined,
disposition: foreign ? src.disposition : undefined,
languages: src.languages
};
}
};
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;
}
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;
}
}());

310
src/core/fresh-resume.js Normal file
View File

@ -0,0 +1,310 @@
/**
Definition of the FRESHResume class.
@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')
, CONVERTER = require('./convert');
/**
A FRESH-style resume in JSON or YAML.
@class FreshResume
*/
function FreshResume() {
}
/**
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.
*/
FreshResume.prototype.open = function( file, 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.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
return this;
};
/**
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
FreshResume.prototype.saveAs = function( filename, format ) {
this.imp.fileName = filename || this.imp.fileName;
if( format !== 'JRS' ) {
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
}
else {
var newRep = CONVERTER.toJRS( this );
FS.writeFileSync( this.imp.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(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
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 );
};
/**
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.
*/
FreshResume.prototype.parse = function( stringData, opts ) {
// Parse the incoming JSON representation
var rep = JSON.parse( stringData );
// Convert JSON Resume to FRESH if necessary
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.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 );
(opts.sort === undefined || opts.sort) && this.sort();
(opts.compute === undefined || opts.compute) && (this.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
return this;
};
/**
Return a unique list of all keywords across all skills.
*/
FreshResume.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
*/
FreshResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str )
return this;
};
/**
Reset the sheet to an empty state.
*/
FreshResume.prototype.clear = function( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
clearMeta && (delete this.imp);
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.
*/
FreshResume.default = function() {
return new FreshResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' );
}
/**
Add work experience to the sheet.
*/
FreshResume.prototype.add = function( moniker ) {
var defSheet = FreshResume.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).
*/
FreshResume.prototype.hasProfile = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.some( this.social, function(p) {
return p.network.trim().toLowerCase() === socialNetwork;
});
};
/**
Determine if the sheet includes a specific skill.
*/
FreshResume.prototype.hasSkill = function( skill ) {
skill = skill.trim().toLowerCase();
return this.skills && _.some( this.skills, function(sk) {
return sk.keywords && _.some( sk.keywords, function(kw) {
return kw.trim().toLowerCase() === skill;
});
});
};
/**
Validate the sheet against the FRESH Resume schema.
*/
FreshResume.prototype.isValid = function( info ) {
var schemaObj = require('FRESCA');
var validator = require('is-my-json-valid')
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.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
}
return ret;
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
FreshResume.prototype.duration = function() {
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().
*/
FreshResume.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.recognition && this.recognition.forEach( function(rec) {
rec.safe = {
date: _fmt( rec.date )
};
});
this.writing && this.writing.forEach( function(pub) {
pub.safe = {
date: _fmt( pub.date )
};
});
}
/**
Export the Sheet function/ctor.
*/
module.exports = FreshResume;
}());
// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
// in addition to YYYY-MM-DD. The original regex:
//
// /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/
//

View File

@ -1,6 +1,6 @@
/**
Abstract character/resume sheet representation.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the JRSResume class.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
*/
(function() {
@ -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,18 +29,18 @@ 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 ) {
this.meta = { fileName: file };
this.meta.raw = FS.readFileSync( file, 'utf8' );
return this.parse( this.meta.raw, title );
JRSResume.prototype.open = function( file, 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).
*/
Sheet.prototype.save = function( filename ) {
this.meta.fileName = filename || this.meta.fileName;
FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' );
JRSResume.prototype.save = function( filename ) {
this.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.imp.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,14 +64,14 @@ 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 );
// 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 );
@ -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,9 +108,9 @@ 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);
clearMeta && (delete this.imp);
delete this.computed; // Don't use Object.keys() here
delete this.work;
delete this.volunteer;
@ -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;
}());

View File

@ -1,6 +1,7 @@
/**
Internal resume generation logic for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@module fluentcmd.js
*/
module.exports = function () {
@ -9,11 +10,12 @@ 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')
, MKDIRP = require('mkdirp')
//, COLORS = require('colors')
, rez, _log, _err;
/**
@ -24,7 +26,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;
@ -35,23 +37,20 @@ module.exports = function () {
// Load input resumes...
if(!src || !src.length) { throw { fluenterror: 3 }; }
var sheets = src.map( function( res ) {
_log( 'Reading JSON resume: ' + res );
return (new FLUENT.Sheet()).open( res );
});
var sheets = loadSourceResumes( src );
// 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 '.gray + a.imp.fileName : '')
+ ' onto '.gray + b.imp.fileName;
return extend( true, b, a );
});
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 +58,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 '.info + theTheme.name.toUpperCase().infoBold + (' theme (' +
Object.keys(theTheme.formats).length + ' formats)').info );
// 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 +95,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 '.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];
MKDIRP.sync( path.dirname(fOut) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, fOut, _opts );
}
catch( ex ) {
@ -109,6 +119,128 @@ module.exports = function () {
throw ex;
}
/**
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: 6 }; }
var isValid = true;
var validator = require('is-my-json-valid');
var schemas = {
fresh: require('FRESCA'),
jars: require('./core/resume.json')
};
// Load input resumes...
var sheets = loadSourceResumes(src, function( res ) {
try {
return {
file: res,
raw: FS.readFileSync( res, 'utf8' )
};
}
catch( ex ) {
throw ex;
}
});
sheets.forEach( function( rep ) {
try {
var rez = JSON.parse( rep.raw );
}
catch( ex ) {
_log('Validating '.info + rep.file.infoBold + ' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold);
if (ex instanceof SyntaxError) {
// Invalid JSON
_log( '--> '.bold.red + rep.file.toUpperCase().red + ' contains invalid JSON. Unable to validate.'.red );
_log( (' INTERNAL: ' + ex).red );
}
else {
_log(('ERROR: ' + ex.toString()).red.bold);
}
return;
}
var isValid = false;
var style = 'useful';
var errors = [];
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 );
});
});
}
/**
Convert between FRESH and JRS formats.
*/
function convert( src, dst, opts, logger ) {
_log = logger || console.log;
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 '.info + 'SOURCE'.infoBold + ' resume: '.info + res.cyan.bold );
return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res );
});
}
/**
Supported resume formats.
*/
@ -139,10 +271,18 @@ module.exports = function () {
Internal module interface. Used by FCV Desktop and HMR.
*/
return {
generate: gen,
verbs: {
build: generate,
validate: validate,
convert: convert,
help: help
},
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

View File

@ -1,10 +1,12 @@
/**
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 = {
Sheet: require('./core/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'),

View File

@ -29,7 +29,9 @@ Base resume generator for FluentCV.
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3
resumeNotFound: 3,
missingCommand: 4,
invalidCommand: 5
},
/**

View File

@ -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;
}

View File

@ -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\>|\<\/p\>\s*$/gi, ''); },
lower: function( txt ) { return txt.toLowerCase(); }
lower: function( txt ) { return txt.toLowerCase(); },
link: function( name, url ) { return url ?
'<a href="' + url + '">' + name + '</a>' : name }
},
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
@ -125,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 || '' });

View File

@ -1,14 +1,19 @@
#! /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' )
, FCMD = require( './fluentcmd')
, PKG = require('../package.json')
, opts = { };
, COLORS = require('colors')
, FS = require('fs')
, PATH = require('path')
, opts = { }
, title = ('*** FluentCV v' + PKG.version + ' ***').bold.white
, _ = require('underscore');
@ -23,20 +28,53 @@ catch( ex ) {
function main() {
// 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 );
// 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 );
// 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
// Generate!
FCMD.generate( src, dst, opts, logMsg );
// 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: "'.warn + verb.warn.bold + '"'.warn);
return;
}
// Get source and dest params
var splitAt = _.indexOf( params, 'to' );
if( splitAt === a._.length - 1 ) {
// 'TO' cannot be the last argument
logMsg('Please '.warn + 'specify an output file'.warnBold +
' for this operation or '.warn + 'omit the TO keyword'.warnBold + '.'.warn );
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 = [ src, dst, opts, logMsg ];
// Invoke the action
FCMD.verbs[ verb ].apply( null, parms );
}
function logMsg( msg ) {
@ -55,13 +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 specify a valid JSON resume file."; break;
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 '.guide : '')
+ v.toUpperCase().guide;
}).join(', '.guide) + ") to get started.\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold;
break;
//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();
@ -70,7 +122,11 @@ function handleError( ex ) {
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
console.log( 'ERROR: ' + trimmed.toString() );
if( !ex.fluenterror || ex.fluenterror < 3 )
console.log( ('ERROR: ' + trimmed.toString()).red.bold );
else
console.log( trimmed.toString() );
process.exit( exitCode );
}

22
src/use.txt Normal file
View File

@ -0,0 +1,22 @@
Usage:
fluentcv <COMMAND> <SOURCES> [TO <TARGETS>] [-t <THEME>]
<COMMAND> should be BUILD, CONVERT, VALIDATE, or HELP. <SOURCES> should
be the path to one or more FRESH or JSON Resume format resumes. <TARGETS>
should be the name of the destination resume to be created, if any. The
<THEME> 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.

View File

@ -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."
}
]
}

36
tests/test-converter.js Normal file
View File

@ -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);
});
});

73
tests/test-fresh-sheet.js Normal file
View File

@ -0,0 +1,73 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, validator = require('is-my-json-valid');
chai.config.includeStack = false;
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/exemplar/jane-doe.json' );
}
tryOpen.should.not.Throw();
});
it('should have one or more of each section', function() {
expect(
//(_sheet.basics) &&
_sheet.name && _sheet.info && _sheet.location && _sheet.contact &&
(_sheet.employment.history && _sheet.employment.history.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.writing && _sheet.writing.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 );
});
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/jane-doe.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new FRESHResume().open('tests/sandbox/jane-doe.json');
_sheet.stringify().should.equal( savedSheet.stringify() )
});
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.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", _sheet.imp.validationErrors, "\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);
// });
// });

View File

@ -4,18 +4,18 @@ 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;
describe('fullstack.json', function () {
describe('fullstack.json (JRS)', function () {
var _sheet;
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,14 +44,14 @@ describe('fullstack.json', 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() )
});
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 );