1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-11-22 08:20:11 +00: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/ node_modules/
tests/sandbox/ tests/sandbox/
docs/

View File

@ -15,15 +15,33 @@ module.exports = function (grunt) {
reporter: 'spec' reporter: 'spec'
}, },
all: { src: ['tests/*.js'] } 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.initConfig( opts );
grunt.loadNpmTasks('grunt-simple-mocha'); 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.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.* *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) ![](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 ## Features
- Runs on OS X, Linux, and Windows. - Runs on OS X, Linux, and Windows.
- Store your resume data as a durable, versionable JSON, YML, or XML document. - Store your resume data as a durable, versionable JSON or YAML document.
- Generate multiple targeted resumes in multiple formats, based on your needs. - Generate polished resumes in multiple formats without violating [DRY][dry].
- Output to HTML, PDF, Markdown, Word, JSON, YAML, XML, or a custom format. - Output to HTML, PDF, Markdown, MS Word, JSON, YAML, plain text, or XML.
- Never update one piece of information in four different resumes again. - Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and
- Compatible with the [JSON Resume standard][6] and [authoring tools][7]. [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. - Free and open-source through the MIT license.
- Forthcoming: StackOverflow and LinkedIn support. - 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 ## Install
FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then: 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. 1. Install the latest official [wkhtmltopdf][3] binary for your platform.
2. Install **fluentCV** by running `npm install fluentcv -g`. 2. Install **fluentCV** with `[sudo] npm install fluentcv -g`.
3. You're ready to go. 3. You're ready to go.
## Use ## 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 ```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 ```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.) # 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 # Generate a specific resume format
fluentcv resume.json -o out/resume.html fluentcv build resume.json TO out/resume.html
fluentcv resume.json -o out/resume.pdf fluentcv build resume.json TO out/resume.pdf
fluentcv resume.json -o out/resume.md fluentcv build resume.json TO out/resume.md
fluentcv resume.json -o out/resume.doc fluentcv build resume.json TO out/resume.doc
fluentcv resume.json -o out/resume.json fluentcv build resume.json TO out/resume.json
fluentcv resume.json -o out/resume.txt fluentcv build resume.json TO out/resume.txt
fluentcv resume.json -o out/resume.yml fluentcv build resume.json TO out/resume.yml
# Specify 2 inputs and 3 outputs # 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: You should see something to the effect of:
``` ```
*** FluentCV v0.7.2 *** *** FluentCV v0.9.0 ***
Reading JSON resume: foo/resume.json Reading JSON resume: foo/resume.json
Applying MODERN Theme (7 formats) Applying MODERN Theme (7 formats)
Generating HTML resume: out/resume.html 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. 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 ```bash
fluentcv resume.json -t modern fluentcv build resume.json -t modern
fluentcv resume.json -t ~/foo/bar/my-custom-theme/ 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 ### Merging resumes
@ -89,13 +159,13 @@ You can **merge multiple resumes together** by specifying them in order from mos
```bash ```bash
# Merge specific.json onto base.json and generate all formats # 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: 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 ```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: in1.json
Reading JSON resume: in2.json Reading JSON resume: in2.json
Reading JSON resume: in3.json Reading JSON resume: in3.json
@ -111,14 +181,14 @@ You can specify **multiple output targets** and FluentCV will build them:
```bash ```bash
# Generate out1.doc, out1.pdf, and foo.txt from me.json. # 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: You can also omit the output file(s) and/or theme completely:
```bash ```bash
# Equivalent to "fluentcv resume.json resume.all -t modern" # Equivalent to "fluentcv resume.json resume.all -t modern"
fluentcv resume.json fluentcv build resume.json
``` ```
### Using .all ### Using .all
@ -127,17 +197,52 @@ The special `.all` extension tells FluentCV to generate all supported output for
```bash ```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, etc.) # 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`. ..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 ```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 ### Silent Mode
@ -145,8 +250,8 @@ fluentcv resume.json out.all --nopretty
Use `-s` or `--silent` to run in silent mode: Use `-s` or `--silent` to run in silent mode:
```bash ```bash
fluentcv resume.json -o someFile.all -s fluentcv generate resume.json -o someFile.all -s
fluentcv resume.json -o someFile.all --silent fluentcv generate resume.json -o someFile.all --silent
``` ```
## License ## License
@ -163,3 +268,6 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
[8]: https://youtu.be/N9wsjroVlu8 [8]: https://youtu.be/N9wsjroVlu8
[9]: https://api.jquery.com/jquery.extend/ [9]: https://api.jquery.com/jquery.extend/
[10]: https://github.com/beautify-web/js-beautify [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", "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.", "description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.",
"repository": { "repository": {
"type": "git", "type": "git",
@ -10,7 +10,15 @@
"resume", "resume",
"CV", "CV",
"portfolio", "portfolio",
"Markdown" "employment",
"career",
"Markdown",
"JSON",
"Word",
"PDF",
"YAML",
"HTML",
"CLI"
], ],
"author": "James M. Devlin", "author": "James M. Devlin",
"license": "MIT", "license": "MIT",
@ -24,7 +32,9 @@
}, },
"homepage": "https://github.com/fluentdesk/fluentcv", "homepage": "https://github.com/fluentdesk/fluentcv",
"dependencies": { "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", "fs-extra": "^0.24.0",
"html": "0.0.10", "html": "0.0.10",
"is-my-json-valid": "^2.12.2", "is-my-json-valid": "^2.12.2",
@ -41,6 +51,7 @@
"devDependencies": { "devDependencies": {
"chai": "*", "chai": "*",
"grunt": "*", "grunt": "*",
"grunt-contrib-yuidoc": "^0.10.0",
"grunt-simple-mocha": "*", "grunt-simple-mocha": "*",
"is-my-json-valid": "^2.12.2", "is-my-json-valid": "^2.12.2",
"mocha": "*", "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. Definition of the JRSResume class.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved. @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
*/ */
(function() { (function() {
@ -13,14 +13,14 @@ Abstract character/resume sheet representation.
, moment = require('moment'); , 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 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, 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 plus a complement of JSON properties from the backing JSON file. That allows
us to treat Sheet objects interchangeably with the loaded JSON model. 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 & onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending. 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.imp = { fileName: file };
this.meta.raw = FS.readFileSync( file, 'utf8' ); this.imp.raw = FS.readFileSync( file, 'utf8' );
return this.parse( this.meta.raw, title ); return this.parse( this.imp.raw, title );
}; };
/** /**
Save the sheet to disk (for environments that have disk access). 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; this.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' ); FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
return this; return this;
}; };
@ -48,7 +48,7 @@ Abstract character/resume sheet representation.
Convert this object to a JSON string, sanitizing meta-properties along the Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString(). way. Don't override .toString().
*/ */
Sheet.prototype.stringify = function() { JRSResume.prototype.stringify = function() {
function replacer( key,value ) { // Exclude these keys from stringification function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', '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 & onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending. consistent format. Then sort each section by startDate descending.
*/ */
Sheet.prototype.parse = function( stringData, opts ) { JRSResume.prototype.parse = function( stringData, opts ) {
opts = opts || { }; opts = opts || { };
var rep = JSON.parse( stringData ); var rep = JSON.parse( stringData );
extend( true, this, rep ); extend( true, this, rep );
// Set up metadata // Set up metadata
if( opts.meta === undefined || opts.meta ) { if( opts.imp === undefined || opts.imp ) {
this.meta = this.meta || { }; this.imp = this.imp || { };
this.meta.title = (opts.title || this.meta.title) || this.basics.name; this.imp.title = (opts.title || this.imp.title) || this.basics.name;
} }
// Parse dates, sort dates, and calculate computed values // Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this ); (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. Return a unique list of all keywords across all skills.
*/ */
Sheet.prototype.keywords = function() { JRSResume.prototype.keywords = function() {
var flatSkills = []; var flatSkills = [];
if( this.skills && this.skills.length ) { if( this.skills && this.skills.length ) {
this.skills.forEach( function( s ) { this.skills.forEach( function( s ) {
@ -99,7 +99,7 @@ Abstract character/resume sheet representation.
/** /**
Update the sheet's raw data. TODO: remove/refactor Update the sheet's raw data. TODO: remove/refactor
*/ */
Sheet.prototype.updateData = function( str ) { JRSResume.prototype.updateData = function( str ) {
this.clear( false ); this.clear( false );
this.parse( str ) this.parse( str )
return this; return this;
@ -108,9 +108,9 @@ Abstract character/resume sheet representation.
/** /**
Reset the sheet to an empty state. Reset the sheet to an empty state.
*/ */
Sheet.prototype.clear = function( clearMeta ) { JRSResume.prototype.clear = function( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || 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.computed; // Don't use Object.keys() here
delete this.work; delete this.work;
delete this.volunteer; delete this.volunteer;
@ -125,15 +125,15 @@ Abstract character/resume sheet representation.
/** /**
Get the default (empty) sheet. Get the default (empty) sheet.
*/ */
Sheet.default = function() { JRSResume.default = function() {
return new Sheet().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); return new JRSResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' );
} }
/** /**
Add work experience to the sheet. Add work experience to the sheet.
*/ */
Sheet.prototype.add = function( moniker ) { JRSResume.prototype.add = function( moniker ) {
var defSheet = Sheet.default(); var defSheet = JRSResume.default();
var newObject = $.extend( true, {}, defSheet[ moniker ][0] ); var newObject = $.extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || []; this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject ); this[ moniker ].push( newObject );
@ -143,7 +143,7 @@ Abstract character/resume sheet representation.
/** /**
Determine if the sheet includes a specific social profile (eg, GitHub). 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(); socialNetwork = socialNetwork.trim().toLowerCase();
return this.basics.profiles && _.some( this.basics.profiles, function(p) { return this.basics.profiles && _.some( this.basics.profiles, function(p) {
return p.network.trim().toLowerCase() === socialNetwork; return p.network.trim().toLowerCase() === socialNetwork;
@ -153,7 +153,7 @@ Abstract character/resume sheet representation.
/** /**
Determine if the sheet includes a specific skill. Determine if the sheet includes a specific skill.
*/ */
Sheet.prototype.hasSkill = function( skill ) { JRSResume.prototype.hasSkill = function( skill ) {
skill = skill.trim().toLowerCase(); skill = skill.trim().toLowerCase();
return this.skills && _.some( this.skills, function(sk) { return this.skills && _.some( this.skills, function(sk) {
return sk.keywords && _.some( sk.keywords, function(kw) { 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. 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 schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' );
var schemaObj = JSON.parse( schema ); var schemaObj = JSON.parse( schema );
var validator = require('is-my-json-valid') 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 *latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs. sheets that have overlapping jobs.
*/ */
Sheet.prototype.duration = function() { JRSResume.prototype.duration = function() {
if( this.work && this.work.length ) { if( this.work && this.work.length ) {
var careerStart = this.work[ this.work.length - 1].safeStartDate; var careerStart = this.work[ this.work.length - 1].safeStartDate;
if ((typeof careerStart === 'string' || careerStart instanceof String) && 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 Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates(). on the sheet have been processed with _parseDates().
*/ */
Sheet.prototype.sort = function( ) { JRSResume.prototype.sort = function( ) {
this.work && this.work.sort( byDateDesc ); this.work && this.work.sort( byDateDesc );
this.education && this.education.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. 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 () { module.exports = function () {
@ -9,11 +10,12 @@ module.exports = function () {
var path = require( 'path' ) var path = require( 'path' )
, extend = require( './utils/extend' ) , extend = require( './utils/extend' )
, unused = require('./utils/string') , unused = require('./utils/string')
, fs = require('fs') , FS = require('fs')
, _ = require('underscore') , _ = require('underscore')
, FLUENT = require('./fluentlib') , FLUENT = require('./fluentlib')
, PATH = require('path') , PATH = require('path')
, MKDIRP = require('mkdirp') , MKDIRP = require('mkdirp')
//, COLORS = require('colors')
, rez, _log, _err; , rez, _log, _err;
/** /**
@ -24,7 +26,7 @@ module.exports = function () {
@param theme Friendly name of the resume theme. Defaults to "modern". @param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override. @param logger Optional logging override.
*/ */
function gen( src, dst, opts, logger, errHandler ) { function generate( src, dst, opts, logger, errHandler ) {
_log = logger || console.log; _log = logger || console.log;
_err = errHandler || error; _err = errHandler || error;
@ -35,23 +37,20 @@ module.exports = function () {
// Load input resumes... // Load input resumes...
if(!src || !src.length) { throw { fluenterror: 3 }; } if(!src || !src.length) { throw { fluenterror: 3 }; }
var sheets = src.map( function( res ) { var sheets = loadSourceResumes( src );
_log( 'Reading JSON resume: ' + res );
return (new FLUENT.Sheet()).open( res );
});
// Merge input resumes... // Merge input resumes...
var msg = ''; var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) { rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ? 'Merging ' + a.meta.fileName : '') msg += ((idx == sheets.length - 2) ? 'Merging '.gray + a.imp.fileName : '')
+ ' onto ' + b.meta.fileName; + ' onto '.gray + b.imp.fileName;
return extend( true, b, a ); return extend( true, b, a );
}); });
msg && _log(msg); msg && _log(msg);
// Load the active theme
// Verify the specified theme name/path // 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'); var exists = require('./utils/file-exists');
if (!exists( tFolder )) { if (!exists( tFolder )) {
tFolder = PATH.resolve( _opts.theme ); tFolder = PATH.resolve( _opts.theme );
@ -59,18 +58,27 @@ module.exports = function () {
throw { fluenterror: 1, data: _opts.theme }; throw { fluenterror: 1, data: _opts.theme };
} }
} }
// Load the theme
var theTheme = new FLUENT.Theme().open( tFolder ); var theTheme = new FLUENT.Theme().open( tFolder );
_opts.themeObj = theTheme; _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) // Expand output resumes... (can't use map() here)
var targets = []; var targets = [], that = this;
var that = this;
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { ( (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' ? 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 } }) Object.keys( theTheme.formats ).map(function(k){
: [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); 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! // Run the transformation!
@ -87,14 +95,16 @@ module.exports = function () {
*/ */
function single( fi, theme ) { function single( fi, theme ) {
try { 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 fObj = _.property( fi.fmt.pre )( theme.formats );
var fOut = path.join( f.substring( 0, f.lastIndexOf('.') + 1 ) + fObj.pre ); 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 ) { _log( 'Generating '.useful + fi.fmt.title.toUpperCase().useful.bold + ' resume: '.useful +
return fmt.name === fi.fmt.pre; path.relative(process.cwd(), f ).useful.bold );
})[0];
MKDIRP( path.dirname(fOut) ); // Ensure dest folder exists; don't bug user 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 ); theFormat.gen.generate( rez, fOut, _opts );
} }
catch( ex ) { catch( ex ) {
@ -109,6 +119,128 @@ module.exports = function () {
throw ex; 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. Supported resume formats.
*/ */
@ -139,10 +271,18 @@ module.exports = function () {
Internal module interface. Used by FCV Desktop and HMR. Internal module interface. Used by FCV Desktop and HMR.
*/ */
return { return {
generate: gen, verbs: {
build: generate,
validate: validate,
convert: convert,
help: help
},
lib: require('./fluentlib'), lib: require('./fluentlib'),
options: _opts, options: _opts,
formats: _fmts 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. External API surface for FluentCV:CLI.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved. @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
*/ */
module.exports = { 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'), Theme: require('./core/theme'),
FluentDate: require('./core/fluent-date'), FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./gen/html-generator'), HtmlGenerator: require('./gen/html-generator'),

View File

@ -29,7 +29,9 @@ Base resume generator for FluentCV.
success: 0, success: 0,
themeNotFound: 1, themeNotFound: 1,
copyCss: 2, 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 ) { invoke: function( rez ) {
// TODO: merge with FCVD // TODO: merge with FCVD
function replacer( key,value ) { // Exclude these keys from stringification 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', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview'], 'isModified', 'htmlPreview', 'safe' ],
function( val ) { return key.trim() === val; } function( val ) { return key.trim() === val; }
) ? undefined : value; ) ? undefined : value;
} }

View File

@ -31,7 +31,9 @@ var _defaultOpts = {
xml: function( txt ) { return XML(txt); }, xml: function( txt ) { return XML(txt); },
md: function( txt ) { return MD(txt); }, md: function( txt ) { return MD(txt); },
mdin: function( txt ) { return MD(txt).replace(/^\s*\<p\>|\<\/p\>\s*$/gi, ''); }, 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 prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2, indent_size: 2,
@ -125,7 +127,6 @@ var TemplateGenerator = module.exports = BaseGenerator.extend({
// Strip {# comments #} // Strip {# comments #}
jst = jst.replace( _.templateSettings.comment, ''); jst = jst.replace( _.templateSettings.comment, '');
json.display_progress_bar = true;
// Compile and run the template. TODO: avoid unnecessary recompiles. // Compile and run the template. TODO: avoid unnecessary recompiles.
jst = _.template(jst)({ r: json, filt: this.opts.filters, cssInfo: cssInfo, headFragment: this.opts.headFragment || '' }); 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 #! /usr/bin/env node
/** /**
Command-line interface (CLI) for FluentCV via Node.js. Command-line interface (CLI) for FluentCV:CLI.
@license Copyright (c) 2015 | James M. Devlin @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
*/ */
var ARGS = require( 'minimist' ) var ARGS = require( 'minimist' )
, FCMD = require( './fluentcmd') , FCMD = require( './fluentcmd')
, PKG = require('../package.json') , 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() { function main() {
// Setup. // Colorize
var title = '*** FluentCV v' + PKG.version + ' ***'; COLORS.setTheme({
if( process.argv.length <= 2 ) { logMsg(title); throw { fluenterror: 3 }; } title: ['white','bold'],
var args = ARGS( process.argv.slice(2) ); info: process.platform === 'win32' ? 'gray' : ['white','dim'],
opts = getOpts( args ); 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 ); 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! // Get the action to be performed
FCMD.generate( src, dst, opts, logMsg ); 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 ) { function logMsg( msg ) {
@ -55,13 +93,27 @@ function getOpts( args ) {
function handleError( ex ) { function handleError( ex ) {
var msg = '', exitCode; var msg = '', exitCode;
if( ex.fluenterror ){ if( ex.fluenterror ){
switch( ex.fluenterror ) { // TODO: Remove magic numbers switch( ex.fluenterror ) { // TODO: Remove magic numbers
case 1: msg = "The specified theme couldn't be found: " + ex.data; break; 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 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; exitCode = ex.fluenterror;
} }
else { else {
msg = ex.toString(); msg = ex.toString();
@ -70,7 +122,11 @@ function handleError( ex ) {
var idx = msg.indexOf('Error: '); var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); 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 ); 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() , should = chai.should()
, path = require('path') , path = require('path')
, _ = require('underscore') , _ = require('underscore')
, Sheet = require('../src/core/sheet') , JRSResume = require('../src/core/jrs-resume')
, validator = require('is-my-json-valid'); , validator = require('is-my-json-valid');
chai.config.includeStack = false; chai.config.includeStack = false;
describe('fullstack.json', function () { describe('fullstack.json (JRS)', function () {
var _sheet; var _sheet;
it('should open without throwing an exception', function () { it('should open without throwing an exception', function () {
function tryOpen() { 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(); tryOpen.should.not.Throw();
}); });
@ -44,14 +44,14 @@ describe('fullstack.json', function () {
}); });
it('should not be modified after saving', 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() ) _sheet.stringify().should.equal( savedSheet.stringify() )
}); });
it('should validate against the JSON Resume schema', function() { it('should validate against the JSON Resume schema', function() {
var schemaJson = require('../src/core/resume.json'); var schemaJson = require('../src/core/resume.json');
var validate = validator( schemaJson, { verbose: true } ); 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. " + result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", validate.errors, "\n\n"); "Validation errors:\n\n", validate.errors, "\n\n");
result.should.equal( true ); result.should.equal( true );