mirror of
https://github.com/JuanCanham/HackMyResume.git
synced 2024-11-25 09:50:11 +00:00
commit
80315f12ac
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
tests/sandbox/
|
||||
docs/
|
||||
|
22
Gruntfile.js
22
Gruntfile.js
@ -15,15 +15,33 @@ module.exports = function (grunt) {
|
||||
reporter: 'spec'
|
||||
},
|
||||
all: { src: ['tests/*.js'] }
|
||||
},
|
||||
|
||||
yuidoc: {
|
||||
compile: {
|
||||
name: '<%= pkg.name %>',
|
||||
description: '<%= pkg.description %>',
|
||||
version: '<%= pkg.version %>',
|
||||
url: '<%= pkg.homepage %>',
|
||||
options: {
|
||||
paths: 'src/',
|
||||
//themedir: 'path/to/custom/theme/',
|
||||
outdir: 'docs/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
grunt.initConfig( opts );
|
||||
grunt.loadNpmTasks('grunt-simple-mocha');
|
||||
grunt.registerTask('test', 'Test the FluentLib library.', function( config ) {
|
||||
grunt.loadNpmTasks('grunt-contrib-yuidoc');
|
||||
grunt.registerTask('test', 'Test the FluentCV library.', function( config ) {
|
||||
grunt.task.run( ['simplemocha:all'] );
|
||||
});
|
||||
grunt.registerTask('default', [ 'test' ]);
|
||||
grunt.registerTask('document', 'Generate FluentCV library documentation.', function( config ) {
|
||||
grunt.task.run( ['yuidoc'] );
|
||||
});
|
||||
grunt.registerTask('default', [ 'test', 'yuidoc' ]);
|
||||
|
||||
};
|
||||
|
182
README.md
182
README.md
@ -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—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 |
17
package.json
17
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fluentcv",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -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
291
src/core/convert.js
Normal 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
310
src/core/fresh-resume.js
Normal 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}$/
|
||||
//
|
@ -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;
|
||||
|
||||
}());
|
186
src/fluentcmd.js
186
src/fluentcmd.js
@ -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!
|
||||
@ -90,11 +98,13 @@ module.exports = function () {
|
||||
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
|
||||
|
||||
_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
|
||||
|
@ -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'),
|
||||
|
@ -29,7 +29,9 @@ Base resume generator for FluentCV.
|
||||
success: 0,
|
||||
themeNotFound: 1,
|
||||
copyCss: 2,
|
||||
resumeNotFound: 3
|
||||
resumeNotFound: 3,
|
||||
missingCommand: 4,
|
||||
invalidCommand: 5
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 || '' });
|
||||
|
88
src/index.js
88
src/index.js
@ -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
22
src/use.txt
Normal 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.
|
130
tests/jrs-exemplar/richard-hendriks.json
Normal file
130
tests/jrs-exemplar/richard-hendriks.json
Normal 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
36
tests/test-converter.js
Normal 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
73
tests/test-fresh-sheet.js
Normal 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);
|
||||
// });
|
||||
// });
|
@ -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 );
|
Loading…
Reference in New Issue
Block a user