mirror of
https://github.com/JuanCanham/HackMyResume.git
synced 2025-05-12 00:27:08 +01:00
Compare commits
94 Commits
Author | SHA1 | Date | |
---|---|---|---|
37a053722d | |||
12fcf3b0cb | |||
43ad9c1c71 | |||
4f9207a868 | |||
3d1f589bc1 | |||
ae436a3b84 | |||
202bb44c76 | |||
041c609ff0 | |||
712b504168 | |||
bc9f0d468f | |||
2d20077c08 | |||
f61deda4e8 | |||
8203fa50ae | |||
c5eab0fd9c | |||
40e71238ac | |||
9d75b207d1 | |||
9b52c396d3 | |||
2759727984 | |||
e230d640cb | |||
d69688697c | |||
9f7ec62b18 | |||
b1a02918ff | |||
ec05f6737a | |||
da5db6477b | |||
0f580efb2b | |||
ff23ee508b | |||
2819faeb6f | |||
d205e882f6 | |||
3f40441b0a | |||
6185f20ec9 | |||
6a61989eb4 | |||
d658a069cd | |||
25688dbe8e | |||
98362b9687 | |||
4c31c96891 | |||
219209c6ca | |||
eff9fc51cb | |||
2ba23ee80d | |||
0f83f8f5c2 | |||
4ba3a3f2a9 | |||
db486a48aa | |||
2cab1195e8 | |||
ce75f09210 | |||
a8fed1b69b | |||
62ca2020d8 | |||
f65cf8880e | |||
c8d4a3deb3 | |||
d5e2a45034 | |||
2465f2ce1c | |||
f2001bcbb1 | |||
d5afb3eb2e | |||
c711cb7922 | |||
e45e0316f6 | |||
08ab512f4c | |||
f2bf09bf96 | |||
75e2b1c131 | |||
0b7ef16a41 | |||
247eec396c | |||
46c7fa9838 | |||
b3fb2c7130 | |||
c3ec3f28bd | |||
0a8ee721e8 | |||
8d7cf32988 | |||
655ecebaa5 | |||
8fc0fa99d3 | |||
69e8adc1cc | |||
6b3396e01b | |||
a95b52acd0 | |||
47553b6def | |||
e4a549ed30 | |||
dd2148bb92 | |||
d8b9d86896 | |||
889bd4bfc5 | |||
13fc903b2b | |||
8c8dbfed72 | |||
2b669cf35c | |||
5a2d892b85 | |||
37a7c318d5 | |||
43873efcab | |||
bb28e5aa8e | |||
c17261cd25 | |||
49e56cc226 | |||
84ad6cf356 | |||
b96526da31 | |||
cb14452df3 | |||
d54b9a6d6c | |||
6285c2db3b | |||
3453293c79 | |||
fb32cb0d78 | |||
baccb75256 | |||
5c39c1c93d | |||
48cc315fc8 | |||
ea8da6811a | |||
dbda48c16d |
@ -1,5 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.11"
|
||||
- "0.12"
|
||||
- "4.0"
|
||||
|
55
README.md
55
README.md
@ -1,7 +1,9 @@
|
||||
HackMyResume
|
||||
============
|
||||
|
||||
[![Build status][travis-image]][travis-url]
|
||||
[![Latest release][img-release]][latest-release]
|
||||
[![Build status (MASTER)][img-master]][travis-url-master]
|
||||
[![Build status (DEV)][img-dev]][travis-url-dev]
|
||||
|
||||
*Create polished résumés and CVs in multiple formats from your command line or
|
||||
shell. Author in clean Markdown and JSON, export to Word, HTML, PDF, LaTeX,
|
||||
@ -25,6 +27,8 @@ or Windows.
|
||||
## Features
|
||||
|
||||
- OS X, Linux, and Windows.
|
||||
- Choose from dozens of FRESH or JSON Resume themes.
|
||||
- Private, local-only resume authoring and analysis.
|
||||
- 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, Markdown, LaTeX, PDF, MS Word, JSON, YAML, plain text, or XML.
|
||||
@ -32,18 +36,23 @@ or Windows.
|
||||
- Support for multiple input and output resumes.
|
||||
- Use from your command line or [desktop][7].
|
||||
- Free and open-source through the MIT license.
|
||||
- Updated daily.
|
||||
- Updated daily / weekly. Contributions welcome.
|
||||
|
||||
## Install
|
||||
|
||||
Install HackMyResume with NPM:
|
||||
Install the latest stable version of HackMyResume with NPM:
|
||||
|
||||
```bash
|
||||
[sudo] npm install hackmyresume -g
|
||||
```
|
||||
|
||||
Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for
|
||||
your platform.
|
||||
To install the latest bleeding-edge version (updated daily):
|
||||
|
||||
```bash
|
||||
[sudo] npm install hacksalot/hackmyresume#dev -g
|
||||
```
|
||||
|
||||
**For PDF generation**, you'll need to install a copy of [wkhtmltopdf][3] and/or PhantomJS for your platform.
|
||||
|
||||
## Installing Themes
|
||||
|
||||
@ -78,7 +87,7 @@ package.json or other NPM/Node infrastructure.
|
||||
|
||||
To use HackMyResume 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 four basic commands you should be aware of:
|
||||
line tool. There are five basic 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
|
||||
@ -99,10 +108,11 @@ formats.
|
||||
hackmyresume NEW r1.json r2.json -f jrs
|
||||
```
|
||||
|
||||
- **analyze** inspects your resume for keywords, duration, and other metrics.
|
||||
|
||||
- **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.
|
||||
formats. Use it to convert between the two formats to take advantage of tools
|
||||
and services.
|
||||
|
||||
```bash
|
||||
# hackmyresume CONVERT <INPUTS> TO <OUTPUTS>
|
||||
@ -170,7 +180,7 @@ hackmyresume BUILD in1.json in2.json TO out.html out.doc out.pdf
|
||||
You should see something to the effect of:
|
||||
|
||||
```
|
||||
*** HackMyResume v0.9.0 ***
|
||||
*** HackMyResume v1.4.0 ***
|
||||
Reading JSON resume: foo/resume.json
|
||||
Applying MODERN Theme (7 formats)
|
||||
Generating HTML resume: out/resume.html
|
||||
@ -186,16 +196,23 @@ Generating YAML resume: out/resume.yml
|
||||
|
||||
### Applying a theme
|
||||
|
||||
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.
|
||||
HackMyResume can work with any FRESH or JSON Resume theme. To specify a theme
|
||||
when generating your resume, use the `-t` or `--theme` parameter:
|
||||
|
||||
```bash
|
||||
hackmyresume BUILD resume.json TO out/rez.all -t [theme]
|
||||
```
|
||||
|
||||
The `[theme]` parameter can be the name of a predefined theme or the path to any
|
||||
FRESH or JSON Resume theme folder:
|
||||
|
||||
```bash
|
||||
hackmyresume BUILD resume.json TO out/rez.all -t modern
|
||||
hackmyresume BUILD resume.json TO OUT.rez.all -t ~/foo/bar/my-custom-theme/
|
||||
hackmyresume BUILD resume.json TO OUT.rez.all -t ../some-folder/my-custom-theme/
|
||||
hackmyresume BUILD resume.json TO OUT.rez.all -t npm_modules/jsonresume-theme-classy
|
||||
```
|
||||
|
||||
As of v1.0.0, available predefined themes are `positive`, `modern`, `compact`,
|
||||
As of v1.4.0, available predefined themes are `positive`, `modern`, `compact`,
|
||||
`minimist`, and `hello-world`.
|
||||
|
||||
### Merging resumes
|
||||
@ -324,8 +341,12 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
|
||||
[fresh]: https://github.com/fluentdesk/FRESH
|
||||
[fresca]: https://github.com/fluentdesk/FRESCA
|
||||
[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
|
||||
[travis-image]: https://img.shields.io/travis/palomajs/paloma.svg?style=flat-square
|
||||
[travis-url]: https://travis-ci.org/hacksalot/HackMyResume
|
||||
[img-release]: https://img.shields.io/github/release/hacksalot/HackMyResume.svg?label=version
|
||||
[img-master]: https://img.shields.io/travis/hacksalot/HackMyResume/master.svg
|
||||
[img-dev]: https://img.shields.io/travis/hacksalot/HackMyResume/dev.svg?label=dev
|
||||
[travis-url-master]: https://travis-ci.org/hacksalot/HackMyResume?branch=master
|
||||
[travis-url-dev]: https://travis-ci.org/hacksalot/HackMyResume?branch=dev
|
||||
[latest-release]: https://github.com/hacksalot/HackMyResume/releases/latest
|
||||
[contribute]: CONTRIBUTING.md
|
||||
[fresh-themes]: https://github.com/fluentdesk/fresh-themes
|
||||
[jrst]: https://www.npmjs.com/search?q=jsonresume-theme
|
||||
|
26
package.json
26
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hackmyresume",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -30,8 +30,10 @@
|
||||
],
|
||||
"author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
|
||||
"contributors": [
|
||||
"Edmund Jorgensen (https://github.com/tomheon)",
|
||||
"Ya Zhuang (https://github.com/zhuangya)",
|
||||
"jjanusch (https://github.com/driftdev)",
|
||||
"robertmain (https://github.com/robertmain)",
|
||||
"tomheon (https://github.com/tomheon)",
|
||||
"zhuangya (https://github.com/zhuangya)",
|
||||
"hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)"
|
||||
],
|
||||
"license": "MIT",
|
||||
@ -39,16 +41,18 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/hacksalot/HackMyResume/issues"
|
||||
},
|
||||
"main": "src/hackmycmd.js",
|
||||
"main": "src/hackmyapi.js",
|
||||
"bin": {
|
||||
"hackmyresume": "src/index.js"
|
||||
},
|
||||
"homepage": "https://github.com/hacksalot/HackMyResume",
|
||||
"dependencies": {
|
||||
"colors": "^1.1.2",
|
||||
"chalk": "^1.1.1",
|
||||
"commander": "^2.9.0",
|
||||
"copy": "^0.1.3",
|
||||
"fresca": "~0.2.2",
|
||||
"fresh-themes": "~0.9.3-beta",
|
||||
"fresca": "~0.3.0",
|
||||
"fresh-resume-empty": "^0.1.0",
|
||||
"fresh-themes": "~0.12.0-beta",
|
||||
"fs-extra": "^0.24.0",
|
||||
"handlebars": "^4.0.5",
|
||||
"html": "0.0.10",
|
||||
@ -57,30 +61,32 @@
|
||||
"jst": "0.0.13",
|
||||
"lodash": "^3.10.1",
|
||||
"marked": "^0.3.5",
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.10.6",
|
||||
"parse-filepath": "^0.6.3",
|
||||
"path-exists": "^2.1.0",
|
||||
"phantom": "^0.8.4",
|
||||
"recursive-readdir-sync": "^1.0.6",
|
||||
"simple-html-tokenizer": "^0.2.0",
|
||||
"slash": "^1.0.0",
|
||||
"string-padding": "^1.0.2",
|
||||
"string.prototype.startswith": "^0.2.0",
|
||||
"underscore": "^1.8.3",
|
||||
"webshot": "^0.16.0",
|
||||
"wkhtmltopdf": "^0.1.5",
|
||||
"word-wrap": "^1.1.0",
|
||||
"xml-escape": "^1.0.0",
|
||||
"yamljs": "^0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "*",
|
||||
"fresh-test-resumes": "^0.2.1",
|
||||
"grunt": "*",
|
||||
"grunt-cli": "^0.1.13",
|
||||
"grunt-contrib-clean": "^0.7.0",
|
||||
"grunt-contrib-jshint": "^0.11.3",
|
||||
"grunt-contrib-yuidoc": "^0.10.0",
|
||||
"grunt-simple-mocha": "*",
|
||||
"jane-q-fullstacker": "fluentdesk/jane-q-fullstacker",
|
||||
"johnny-trouble-resume": "fluentdesk/johnny-trouble-resume",
|
||||
"jsonresume-theme-boilerplate": "^0.1.2",
|
||||
"jsonresume-theme-classy": "^1.0.9",
|
||||
"jsonresume-theme-modern": "0.0.18",
|
||||
|
@ -4,10 +4,16 @@ FRESH to JSON Resume conversion routiens.
|
||||
@module convert.js
|
||||
*/
|
||||
|
||||
(function(){
|
||||
|
||||
|
||||
(function(){ // TODO: refactor everything
|
||||
|
||||
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Convert between FRESH and JRS resume/CV formats.
|
||||
@class FRESHConverter
|
||||
@ -15,6 +21,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
var FRESHConverter = module.exports = {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Convert from JSON Resume format to FRESH.
|
||||
@method toFresh
|
||||
@ -25,27 +32,21 @@ FRESH to JSON Resume conversion routiens.
|
||||
foreign = (foreign === undefined || foreign === null) ? true : foreign;
|
||||
|
||||
return {
|
||||
|
||||
name: src.basics.name,
|
||||
|
||||
imp: src.basics.imp,
|
||||
|
||||
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,
|
||||
@ -53,7 +54,6 @@ FRESH to JSON Resume conversion routiens.
|
||||
code: src.basics.location.postalCode,
|
||||
address: src.basics.location.address
|
||||
},
|
||||
|
||||
employment: employment( src.work, true ),
|
||||
education: education( src.education, true),
|
||||
service: service( src.volunteer, true),
|
||||
@ -68,6 +68,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Convert from FRESH format to JSON Resume.
|
||||
@param foreign True if non-JSON-Resume properties should be included in
|
||||
@ -79,7 +81,6 @@ FRESH to JSON Resume conversion routiens.
|
||||
foreign = (foreign === undefined || foreign === null) ? false : foreign;
|
||||
|
||||
return {
|
||||
|
||||
basics: {
|
||||
name: src.name,
|
||||
label: src.info.label,
|
||||
@ -99,7 +100,6 @@ FRESH to JSON Resume conversion routiens.
|
||||
profiles: social( src.social, false ),
|
||||
imp: src.imp
|
||||
},
|
||||
|
||||
work: employment( src.employment, false ),
|
||||
education: education( src.education, false ),
|
||||
skills: skillsToJRS( src.skills, false ),
|
||||
@ -111,20 +111,21 @@ FRESH to JSON Resume conversion routiens.
|
||||
samples: foreign ? src.samples : undefined,
|
||||
disposition: foreign ? src.disposition : undefined,
|
||||
languages: src.languages
|
||||
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
toSTRING: function( src ) {
|
||||
function replacerJRS( key,value ) { // Exclude these keys from stringification
|
||||
function replacerJRS( key,value ) { // Exclude these keys
|
||||
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
|
||||
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
|
||||
'isModified', 'htmlPreview', 'display_progress_bar'],
|
||||
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate',
|
||||
'result', 'isModified', 'htmlPreview', 'display_progress_bar'],
|
||||
function( val ) { return key.trim() === val; }
|
||||
) ? undefined : value;
|
||||
}
|
||||
function replacerFRESH( key,value ) { // Exclude these keys from stringification
|
||||
function replacerFRESH( key,value ) { // Exclude these keys
|
||||
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
|
||||
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'],
|
||||
function( val ) { return key.trim() === val; }
|
||||
@ -136,6 +137,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
function meta( direction, obj ) {
|
||||
//if( !obj ) return obj; // preserve null and undefined
|
||||
if( direction ) {
|
||||
@ -146,6 +149,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function employment( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( !direction ) {
|
||||
@ -170,7 +175,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
position: job.position,
|
||||
employer: job.company,
|
||||
summary: job.summary,
|
||||
current: (!job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current') || undefined,
|
||||
current: (!job.endDate || !job.endDate.trim() ||
|
||||
job.endDate.trim().toLowerCase() === 'current') || undefined,
|
||||
start: job.startDate,
|
||||
end: job.endDate,
|
||||
url: job.website,
|
||||
@ -183,6 +189,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
|
||||
|
||||
|
||||
function education( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
@ -219,6 +226,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function service( obj, direction, foreign ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
@ -254,6 +263,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function social( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
@ -277,6 +288,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function recognition( obj, direction, foreign ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
@ -306,6 +319,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function references( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
@ -328,6 +343,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function writing( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
@ -346,7 +363,8 @@ FRESH to JSON Resume conversion routiens.
|
||||
return obj && obj.length ? obj.map(function(pub){
|
||||
return {
|
||||
name: pub.title,
|
||||
publisher: pub.publisher && pub.publisher.name ? pub.publisher.name : pub.publisher,
|
||||
publisher: pub.publisher &&
|
||||
pub.publisher.name ? pub.publisher.name : pub.publisher,
|
||||
releaseDate: pub.date,
|
||||
website: pub.url,
|
||||
summary: pub.summary
|
||||
@ -355,10 +373,12 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
}
|
||||
|
||||
function skillsToFRESH( skills ) {
|
||||
|
||||
|
||||
function skillsToFRESH( skills ) {
|
||||
if( !skills ) return skills;
|
||||
return {
|
||||
sets: skills.map(function(set) {
|
||||
sets: skills.map(function( set ) {
|
||||
return {
|
||||
name: set.name,
|
||||
level: set.level,
|
||||
@ -368,7 +388,10 @@ FRESH to JSON Resume conversion routiens.
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function skillsToJRS( skills ) {
|
||||
if( !skills ) return skills;
|
||||
var ret = [];
|
||||
if( skills.sets && skills.sets.length ) {
|
||||
ret = skills.sets.map(function(set){
|
||||
|
@ -1,20 +1,20 @@
|
||||
(function(){
|
||||
|
||||
var FLUENT = require('../hackmyapi');
|
||||
|
||||
/**
|
||||
Supported resume formats.
|
||||
*/
|
||||
|
||||
module.exports = [
|
||||
{ name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() },
|
||||
{ name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() },
|
||||
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() },
|
||||
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() },
|
||||
{ name: 'png', ext: 'png', fmt: 'html', is: false, gen: new FLUENT.HtmlPngGenerator() },
|
||||
{ name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() },
|
||||
{ name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() },
|
||||
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() },
|
||||
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() }
|
||||
|
||||
{ name: 'html', ext: 'html', gen: new (require('../gen/html-generator'))() },
|
||||
{ name: 'txt', ext: 'txt', gen: new (require('../gen/text-generator'))() },
|
||||
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../gen/word-generator'))() },
|
||||
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new (require('../gen/html-pdf-generator'))() },
|
||||
{ name: 'png', ext: 'png', fmt: 'html', is: false, gen: new (require('../gen/html-png-generator'))() },
|
||||
{ name: 'md', ext: 'md', fmt: 'txt', gen: new (require('../gen/markdown-generator'))() },
|
||||
{ name: 'json', ext: 'json', gen: new (require('../gen/json-generator'))() },
|
||||
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../gen/json-yaml-generator'))() },
|
||||
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../gen/latex-generator'))() }
|
||||
|
||||
];
|
||||
|
||||
|
@ -1,184 +0,0 @@
|
||||
{
|
||||
|
||||
"name": "",
|
||||
|
||||
"meta": {
|
||||
"format": "FRESH@0.1.0",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
|
||||
"info": {
|
||||
"label": "",
|
||||
"characterClass": "",
|
||||
"brief": "",
|
||||
"image": ""
|
||||
},
|
||||
|
||||
"contact": {
|
||||
"website": "",
|
||||
"phone": "",
|
||||
"email": "",
|
||||
"other": []
|
||||
},
|
||||
|
||||
"location": {
|
||||
"address": "",
|
||||
"city": "",
|
||||
"region": "",
|
||||
"code": "",
|
||||
"country": ""
|
||||
},
|
||||
|
||||
"social": [
|
||||
{
|
||||
"label": "",
|
||||
"network": "",
|
||||
"user": "",
|
||||
"url": ""
|
||||
}
|
||||
],
|
||||
|
||||
"employment": {
|
||||
"summary": "",
|
||||
"history": [
|
||||
{
|
||||
"employer": "",
|
||||
"url": "",
|
||||
"position": "",
|
||||
"summary": "",
|
||||
"start": "",
|
||||
"end": "",
|
||||
"keywords": [],
|
||||
"highlights": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"education": {
|
||||
"summary": "",
|
||||
"level": "",
|
||||
"degree": "",
|
||||
"history": [
|
||||
{
|
||||
"institution": "",
|
||||
"url": "",
|
||||
"start": "",
|
||||
"end": "",
|
||||
"grade": "",
|
||||
"summary": "",
|
||||
"curriculum": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"service": {
|
||||
"summary": "",
|
||||
"history": [
|
||||
{
|
||||
"flavor": "",
|
||||
"position": "",
|
||||
"organization": "",
|
||||
"url": "",
|
||||
"start": "",
|
||||
"end": "",
|
||||
"summary": "",
|
||||
"highlights": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"skills": {
|
||||
|
||||
"sets": [
|
||||
{
|
||||
"name": "",
|
||||
"level": "",
|
||||
"skills": []
|
||||
}
|
||||
],
|
||||
|
||||
"list": [ ]
|
||||
},
|
||||
|
||||
"samples": [
|
||||
{
|
||||
"title": "",
|
||||
"summary": "",
|
||||
"url": "",
|
||||
"date": ""
|
||||
}
|
||||
],
|
||||
|
||||
"writing": [
|
||||
{
|
||||
"title": "",
|
||||
"flavor": "",
|
||||
"date": "",
|
||||
"publisher": {
|
||||
"name": "",
|
||||
"url": ""
|
||||
},
|
||||
"url": ""
|
||||
}
|
||||
],
|
||||
|
||||
"reading": [
|
||||
{
|
||||
"title": "",
|
||||
"flavor": "",
|
||||
"url": "",
|
||||
"author": ""
|
||||
}
|
||||
],
|
||||
|
||||
"recognition": [
|
||||
{
|
||||
"flavor": "",
|
||||
"from": "",
|
||||
"title": "",
|
||||
"event": "",
|
||||
"date": "",
|
||||
"summary": ""
|
||||
}
|
||||
],
|
||||
|
||||
"references": [
|
||||
{
|
||||
"name": "",
|
||||
"flavor": "",
|
||||
"private": true,
|
||||
"contact": [
|
||||
{
|
||||
"label": "",
|
||||
"flavor": "",
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"testimonials": [
|
||||
{
|
||||
"name": "",
|
||||
"flavor": "",
|
||||
"quote": ""
|
||||
}
|
||||
],
|
||||
|
||||
"languages": [
|
||||
{
|
||||
"language": "",
|
||||
"level": "",
|
||||
"years": 0
|
||||
}
|
||||
],
|
||||
|
||||
"interests": [
|
||||
{
|
||||
"name": "",
|
||||
"summary": "",
|
||||
"keywords": []
|
||||
}
|
||||
]
|
||||
|
||||
}
|
@ -1,24 +1,45 @@
|
||||
/**
|
||||
Error-handling routines for HackMyResume.
|
||||
@module error-handler.js
|
||||
@license MIT. See LICENSE.md for details.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
|
||||
|
||||
var HACKMYSTATUS = require('./status-codes')
|
||||
, PKG = require('../../package.json')
|
||||
, FS = require('fs')
|
||||
, FCMD = require('../hackmycmd')
|
||||
, FCMD = require('../hackmyapi')
|
||||
, PATH = require('path')
|
||||
, title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white;
|
||||
, chalk = require('chalk');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
An amorphous blob of error handling code for HackMyResume.
|
||||
@class ErrorHandler
|
||||
*/
|
||||
var ErrorHandler = module.exports = {
|
||||
|
||||
|
||||
|
||||
err: function( ex, shouldExit ) {
|
||||
var msg = '', exitCode;
|
||||
|
||||
if( ex.handled ) {
|
||||
if( shouldExit )
|
||||
process.exit( exitCode );
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if( ex.fluenterror ){
|
||||
switch( ex.fluenterror ) { // TODO: Remove magic numbers
|
||||
|
||||
switch( ex.fluenterror ) {
|
||||
|
||||
case HACKMYSTATUS.themeNotFound:
|
||||
msg = "The specified theme couldn't be found: " + ex.data;
|
||||
@ -29,48 +50,49 @@
|
||||
break;
|
||||
|
||||
case HACKMYSTATUS.resumeNotFound:
|
||||
msg = 'Please '.guide + 'feed me a resume'.guide.bold +
|
||||
' in FRESH or JSON Resume format.'.guide;
|
||||
msg = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
|
||||
chalk.yellow(' in FRESH or JSON Resume format.');
|
||||
break;
|
||||
|
||||
case HACKMYSTATUS.missingCommand:
|
||||
msg = title + "\nPlease ".guide + "give me a command".guide.bold +
|
||||
" (".guide;
|
||||
msg = chalk.yellow("Please ") + chalk.yellow.bold("give me a command") +
|
||||
chalk.yellow(" (");
|
||||
|
||||
msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
|
||||
return (idx === ar.length - 1 ? 'or '.guide : '') +
|
||||
v.toUpperCase().guide;
|
||||
}).join(', '.guide) + ").\n\n".guide;
|
||||
return (idx === ar.length - 1 ? chalk.yellow('or ') : '') +
|
||||
chalk.yellow.bold(v.toUpperCase());
|
||||
}).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
|
||||
|
||||
msg += FS.readFileSync(
|
||||
PATH.resolve(__dirname, '../use.txt'), 'utf8' ).info.bold;
|
||||
msg += chalk.gray(FS.readFileSync( PATH.resolve(__dirname, '../use.txt'), 'utf8' ));
|
||||
break;
|
||||
|
||||
case HACKMYSTATUS.invalidCommand:
|
||||
msg = 'Please '.guide + 'specify the output resume file'.guide.bold +
|
||||
' that should be created.'.guide;
|
||||
msg = chalk.yellow('Invalid command: "') + chalk.yellow.bold(ex.attempted) + chalk.yellow('"');
|
||||
break;
|
||||
|
||||
case HACKMYSTATUS.resumeNotFoundAlt:
|
||||
msg = 'Please '.guide + 'feed me a resume'.guide.bold +
|
||||
' in either FRESH or JSON Resume format.'.guide;
|
||||
msg = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
|
||||
chalk.yellow(' in either FRESH or JSON Resume format.');
|
||||
break;
|
||||
|
||||
case HACKMYSTATUS.inputOutputParity:
|
||||
msg = 'Please '.guide + 'specify an output file name'.guide.bold +
|
||||
' for every input file you wish to convert.'.guide;
|
||||
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify an output file name') +
|
||||
chalk.yellow(' for every input file you wish to convert.');
|
||||
break;
|
||||
|
||||
case HACKMYSTATUS.createNameMissing:
|
||||
msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold +
|
||||
' to create.'.guide;
|
||||
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify the filename of the resume') +
|
||||
chalk.yellow(' to create.');
|
||||
break;
|
||||
|
||||
case HACKMYSTATUS.wkhtmltopdf:
|
||||
msg = 'ERROR: PDF generation failed. '.red.bold + ('Make sure wkhtmltopdf is ' +
|
||||
'installed and accessible from your path.').red;
|
||||
msg = chalk.red.bold('ERROR: PDF generation failed. ') + chalk.red('Make sure wkhtmltopdf is ' +
|
||||
'installed and accessible from your path.');
|
||||
break;
|
||||
|
||||
case HACKMYSTATUS.invalid:
|
||||
msg = chalk.red.bold('ERROR: Validation failed and the --assert option was specified.');
|
||||
break;
|
||||
}
|
||||
exitCode = ex.fluenterror;
|
||||
|
||||
@ -80,17 +102,23 @@
|
||||
exitCode = 4;
|
||||
}
|
||||
|
||||
// Deal with pesky 'Error:' prefix.
|
||||
var idx = msg.indexOf('Error: ');
|
||||
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
|
||||
|
||||
// If this is an unhandled error, or a specific class of handled error,
|
||||
// output the error message and stack.
|
||||
if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s
|
||||
console.log( ('ERROR: ' + trimmed.toString()).red.bold );
|
||||
console.log( ex.stack.gray);
|
||||
console.log( chalk.red.bold('ERROR: ' + trimmed.toString()) );
|
||||
if( ex.code !== 'ENOENT' ) // Don't emit stack for common stuff
|
||||
console.log( chalk.gray(ex.stack) );
|
||||
}
|
||||
else {
|
||||
console.log( trimmed.toString() );
|
||||
}
|
||||
|
||||
if( shouldExit )
|
||||
// Let the error code be the process's return code.
|
||||
if( shouldExit || ex.shouldExit )
|
||||
process.exit( exitCode );
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
/**
|
||||
Definition of the FRESHResume class.
|
||||
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
|
||||
@license MIT. See LICENSE .md for details.
|
||||
@module fresh-resume.js
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
|
||||
|
||||
var FS = require('fs')
|
||||
, extend = require('../utils/extend')
|
||||
, validator = require('is-my-json-valid')
|
||||
@ -18,18 +22,21 @@ Definition of the FRESHResume class.
|
||||
, CONVERTER = require('./convert')
|
||||
, JRSResume = require('./jrs-resume');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
A FRESH-style resume in JSON or YAML.
|
||||
A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
|
||||
object is an instantiation of that JSON decorated with utility methods.
|
||||
@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.
|
||||
Initialize the FreshResume from file.
|
||||
*/
|
||||
FreshResume.prototype.open = function( file, title ) {
|
||||
this.imp = { fileName: file };
|
||||
@ -37,192 +44,19 @@ Definition of the FRESHResume class.
|
||||
return this.parse( this.imp.raw, title );
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Save the sheet to disk (for environments that have disk access).
|
||||
Initialize the the FreshResume from string.
|
||||
*/
|
||||
FreshResume.prototype.save = function( filename ) {
|
||||
this.imp.fileName = filename || this.imp.fileName;
|
||||
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
|
||||
return this;
|
||||
FreshResume.prototype.parse = function( stringData, opts ) {
|
||||
return this.parseJSON( JSON.parse( stringData ), opts );
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
|
||||
*/
|
||||
FreshResume.prototype.saveAs = function( filename, format ) {
|
||||
|
||||
if( format !== 'JRS' ) {
|
||||
this.imp.fileName = filename || this.imp.fileName;
|
||||
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
|
||||
}
|
||||
else {
|
||||
var newRep = CONVERTER.toJRS( this );
|
||||
FS.writeFileSync( filename, JRSResume.stringify( newRep ), 'utf8' );
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
FreshResume.prototype.dupe = function() {
|
||||
var rnew = new FreshResume();
|
||||
rnew.parse( this.stringify(), { } );
|
||||
return rnew;
|
||||
};
|
||||
|
||||
/**
|
||||
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 );
|
||||
};
|
||||
|
||||
/**
|
||||
Create a copy of this resume in which all string fields have been run through
|
||||
a transformation function (such as a Markdown filter or XML encoder).
|
||||
*/
|
||||
FreshResume.prototype.transformStrings = function( filters, transformer ) {
|
||||
|
||||
var that = this;
|
||||
var ret = this.dupe();
|
||||
|
||||
// TODO: refactor recursion
|
||||
function transformStringsInObject( obj ) {
|
||||
|
||||
if( !obj ) return;
|
||||
if( moment.isMoment( obj ) ) return;
|
||||
|
||||
if( _.isArray( obj ) ) {
|
||||
obj.forEach( function(elem, idx, ar) {
|
||||
if( typeof elem === 'string' || elem instanceof String )
|
||||
ar[idx] = transformer( null, elem );
|
||||
else if (_.isObject(elem))
|
||||
transformStringsInObject( elem );
|
||||
});
|
||||
}
|
||||
else if (_.isObject( obj )) {
|
||||
Object.keys( obj ).forEach(function(k) {
|
||||
var sub = obj[k];
|
||||
if( typeof sub === 'string' || sub instanceof String ) {
|
||||
if( filters.length && _.contains(filters, k) )
|
||||
return;
|
||||
obj[k] = transformer( k, sub );
|
||||
}
|
||||
else if (_.isObject( sub ))
|
||||
transformStringsInObject( sub );
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.keys( ret ).forEach(function(member){
|
||||
transformStringsInObject( ret[ member ] );
|
||||
});
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
Create a copy of this resume in which all fields have been interpreted as
|
||||
Markdown.
|
||||
*/
|
||||
FreshResume.prototype.markdownify = function() {
|
||||
|
||||
function MDIN( txt ){
|
||||
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
|
||||
}
|
||||
|
||||
function trx(key, val) {
|
||||
if( key === 'summary' ) {
|
||||
return MD(val);
|
||||
}
|
||||
return MDIN(val);
|
||||
}
|
||||
|
||||
return this.transformStrings( ['skills','url','start','end','date'], trx );
|
||||
};
|
||||
|
||||
/**
|
||||
Create a copy of this resume in which all fields have been interpreted as
|
||||
Markdown.
|
||||
*/
|
||||
FreshResume.prototype.xmlify = function() {
|
||||
|
||||
function trx(key, val) {
|
||||
return XML(val);
|
||||
}
|
||||
|
||||
return this.transformStrings( [], trx );
|
||||
};
|
||||
|
||||
/**
|
||||
Create a copy of this resume in which all fields have been interpreted as
|
||||
Markdown.
|
||||
*/
|
||||
FreshResume.prototype.markdownify2 = function() {
|
||||
|
||||
var that = this;
|
||||
var ret = this.dupe();
|
||||
|
||||
function MDIN(txt){
|
||||
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
|
||||
}
|
||||
|
||||
// TODO: refactor recursion
|
||||
function markdownifyStringsInObject( obj, inline ) {
|
||||
|
||||
if( !obj ) return;
|
||||
|
||||
inline = inline === undefined || inline;
|
||||
|
||||
if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
|
||||
obj.forEach(function(elem, idx, ar){
|
||||
if( typeof elem === 'string' || elem instanceof String )
|
||||
ar[idx] = inline ? MDIN(elem) : MD( elem );
|
||||
else
|
||||
markdownifyStringsInObject( elem );
|
||||
});
|
||||
}
|
||||
else if (typeof obj === 'object') {
|
||||
Object.keys( obj ).forEach(function(key) {
|
||||
var sub = obj[key];
|
||||
if( typeof sub === 'string' || sub instanceof String ) {
|
||||
if( _.contains(['skills','url','start','end','date'], key) )
|
||||
return;
|
||||
if( key === 'summary' )
|
||||
obj[key] = MD( obj[key] );
|
||||
else
|
||||
obj[key] = inline ? MDIN( obj[key] ) : MD( obj[key] );
|
||||
}
|
||||
else
|
||||
markdownifyStringsInObject( sub );
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.keys( ret ).forEach(function(member){
|
||||
markdownifyStringsInObject( ret[ member ] );
|
||||
});
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
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 );
|
||||
};
|
||||
|
||||
/**
|
||||
Initialize the FreshResume from JSON data.
|
||||
Initialize the FreshResume from JSON.
|
||||
Open and parse the specified FRESH resume. Merge the JSON object model onto
|
||||
this Sheet instance with extend() and convert sheet dates to a safe &
|
||||
consistent format. Then sort each section by startDate descending.
|
||||
@ -254,25 +88,200 @@ Definition of the FRESHResume class.
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Initialize the the FreshResume from string data.
|
||||
Save the sheet to disk (for environments that have disk access).
|
||||
*/
|
||||
FreshResume.prototype.parse = function( stringData, opts ) {
|
||||
return this.parseJSON( JSON.parse( stringData ), opts );
|
||||
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 ) {
|
||||
|
||||
if( format !== 'JRS' ) {
|
||||
this.imp.fileName = filename || this.imp.fileName;
|
||||
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
|
||||
}
|
||||
else {
|
||||
var newRep = CONVERTER.toJRS( this );
|
||||
FS.writeFileSync( filename, JRSResume.stringify( newRep ), 'utf8' );
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Duplicate this FreshResume instance.
|
||||
*/
|
||||
FreshResume.prototype.dupe = function() {
|
||||
var rnew = new FreshResume();
|
||||
rnew.parse( this.stringify(), { } );
|
||||
return rnew;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Convert the supplied FreshResume 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.
|
||||
*/
|
||||
FreshResume.prototype.stringify = function() {
|
||||
return FreshResume.stringify( this );
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Create a copy of this resume in which all string fields have been run through
|
||||
a transformation function (such as a Markdown filter or XML encoder).
|
||||
*/
|
||||
FreshResume.prototype.transformStrings = function( filt, transformer ) {
|
||||
|
||||
var that = this;
|
||||
var ret = this.dupe();
|
||||
|
||||
// TODO: refactor recursion
|
||||
function transformStringsInObject( obj, filters ) {
|
||||
|
||||
if( !obj ) return;
|
||||
if( moment.isMoment( obj ) ) return;
|
||||
|
||||
if( _.isArray( obj ) ) {
|
||||
obj.forEach( function(elem, idx, ar) {
|
||||
if( typeof elem === 'string' || elem instanceof String )
|
||||
ar[idx] = transformer( null, elem );
|
||||
else if (_.isObject(elem))
|
||||
transformStringsInObject( elem, filters );
|
||||
});
|
||||
}
|
||||
else if (_.isObject( obj )) {
|
||||
Object.keys( obj ).forEach(function(k) {
|
||||
if( filters.length && _.contains(filters, k) )
|
||||
return;
|
||||
var sub = obj[k];
|
||||
if( typeof sub === 'string' || sub instanceof String ) {
|
||||
obj[k] = transformer( k, sub );
|
||||
}
|
||||
else if (_.isObject( sub ))
|
||||
transformStringsInObject( sub, filters );
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.keys( ret ).forEach(function(member){
|
||||
if( !filt || !filt.length || !_.contains(filt, member) )
|
||||
transformStringsInObject( ret[ member ], filt || [] );
|
||||
});
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Create a copy of this resume in which all fields have been interpreted as
|
||||
Markdown.
|
||||
*/
|
||||
FreshResume.prototype.markdownify = function() {
|
||||
|
||||
function MDIN( txt ){
|
||||
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
|
||||
}
|
||||
|
||||
function trx(key, val) {
|
||||
if( key === 'summary' ) {
|
||||
return MD(val);
|
||||
}
|
||||
return MDIN(val);
|
||||
}
|
||||
|
||||
return this.transformStrings( ['skills','url','start','end','date'], trx );
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Create a copy of this resume in which all fields have been interpreted as
|
||||
Markdown.
|
||||
*/
|
||||
FreshResume.prototype.xmlify = function() {
|
||||
function trx(key, val) {
|
||||
return XML(val);
|
||||
}
|
||||
return this.transformStrings( [], trx );
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Return the resume format.
|
||||
*/
|
||||
FreshResume.prototype.format = function() {
|
||||
return 'FRESH';
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Return internal metadata. Create if it doesn't exist.
|
||||
*/
|
||||
FreshResume.prototype.i = function() {
|
||||
this.imp = (this.imp || { });
|
||||
return this.imp;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
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; }));
|
||||
if( this.skills ) {
|
||||
if( this.skills.sets ) {
|
||||
flatSkills = this.skills.sets.map(function(sk) { return sk.skills; })
|
||||
.reduce(function(a,b) { return a.concat(b); });
|
||||
}
|
||||
else if( this.skills.list ) {
|
||||
flatSkills = flatSkills.concat( this.skills.list.map(function(sk) {
|
||||
return sk.name;
|
||||
}));
|
||||
}
|
||||
flatSkills = _.uniq( flatSkills );
|
||||
}
|
||||
return flatSkills;
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Reset the sheet to an empty state.
|
||||
Reset the sheet to an empty state. TODO: refactor/review
|
||||
*/
|
||||
FreshResume.prototype.clear = function( clearMeta ) {
|
||||
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
|
||||
@ -289,6 +298,8 @@ Definition of the FRESHResume class.
|
||||
delete this.social;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Get a safe count of the number of things in a section.
|
||||
*/
|
||||
@ -299,14 +310,17 @@ Definition of the FRESHResume class.
|
||||
return obj.length || 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Get the default (empty) sheet.
|
||||
*/
|
||||
FreshResume.default = function() {
|
||||
return new FreshResume().open(
|
||||
PATH.join( __dirname, 'empty-fresh.json'), 'Empty' );
|
||||
return new FreshResume().parseJSON( require('fresh-resume-empty') );
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Add work experience to the sheet.
|
||||
*/
|
||||
@ -327,6 +341,8 @@ Definition of the FRESHResume class.
|
||||
return newObject;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Determine if the sheet includes a specific social profile (eg, GitHub).
|
||||
*/
|
||||
@ -337,6 +353,8 @@ Definition of the FRESHResume class.
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Return the specified network profile.
|
||||
*/
|
||||
@ -347,6 +365,8 @@ Definition of the FRESHResume class.
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Return an array of profiles for the specified network, for when the user
|
||||
has multiple eg. GitHub accounts.
|
||||
@ -358,6 +378,8 @@ Definition of the FRESHResume class.
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Determine if the sheet includes a specific skill.
|
||||
*/
|
||||
@ -370,13 +392,15 @@ Definition of the FRESHResume class.
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
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]
|
||||
var validate = validator( schemaObj, { // See Note [1].
|
||||
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
|
||||
});
|
||||
var ret = validate( this );
|
||||
@ -387,6 +411,8 @@ Definition of the FRESHResume class.
|
||||
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().
|
||||
@ -395,31 +421,48 @@ Definition of the FRESHResume class.
|
||||
*latest end date of all jobs in the work history*. This last condition is for
|
||||
sheets that have overlapping jobs.
|
||||
*/
|
||||
FreshResume.prototype.duration = function() {
|
||||
FreshResume.prototype.duration = function(unit) {
|
||||
unit = unit || 'years';
|
||||
var empHist = __.get(this, 'employment.history');
|
||||
if( empHist && empHist.length ) {
|
||||
var firstJob = _.last( this.employment.history );
|
||||
var firstJob = _.last( empHist );
|
||||
var careerStart = firstJob.start ? firstJob.safe.start : '';
|
||||
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
|
||||
!careerStart.trim())
|
||||
return 0;
|
||||
var careerLast = _.max( this.employment.history, function( w ) {
|
||||
var careerLast = _.max( empHist, function( w ) {
|
||||
return( w.safe && w.safe.end ) ? w.safe.end.unix() : moment().unix();
|
||||
});
|
||||
return careerLast.safe.end.diff( careerStart, 'years' );
|
||||
return careerLast.safe.end.diff( careerStart, unit );
|
||||
}
|
||||
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( ) {
|
||||
|
||||
__.get(this, 'employment.history') && this.employment.history.sort( byDateDesc );
|
||||
__.get(this, 'education.history') && this.education.history.sort( byDateDesc );
|
||||
__.get(this, 'service.history') && this.service.history.sort( byDateDesc );
|
||||
function byDateDesc(a,b) {
|
||||
return( a.safe.start.isBefore(b.safe.start) ) ? 1
|
||||
: ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0;
|
||||
}
|
||||
|
||||
function sortSection( key ) {
|
||||
var ar = __.get(this, key);
|
||||
if( ar && ar.length ) {
|
||||
var datedThings = obj.filter( function(o) { return o.start; } );
|
||||
datedThings.sort( byDateDesc );
|
||||
}
|
||||
}
|
||||
|
||||
sortSection( 'employment.history' );
|
||||
sortSection( 'education.history' );
|
||||
sortSection( 'service.history' );
|
||||
sortSection( 'projects.history' );
|
||||
|
||||
// this.awards && this.awards.sort( function(a, b) {
|
||||
// return( a.safeDate.isBefore(b.safeDate) ) ? 1
|
||||
@ -429,14 +472,10 @@ Definition of the FRESHResume class.
|
||||
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
|
||||
@ -482,11 +521,15 @@ Definition of the FRESHResume class.
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Export the Sheet function/ctor.
|
||||
*/
|
||||
module.exports = FreshResume;
|
||||
|
||||
|
||||
|
||||
}());
|
||||
|
||||
// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
|
||||
|
@ -46,21 +46,33 @@ Definition of the FRESHTheme class.
|
||||
var formatsHash = { };
|
||||
|
||||
// Load the theme
|
||||
var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' );
|
||||
var themeFile = PATH.join( themeFolder, 'theme.json' );
|
||||
var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) );
|
||||
var that = this;
|
||||
|
||||
// Move properties from the theme JSON file to the theme object
|
||||
EXTEND( true, this, themeInfo );
|
||||
|
||||
// Check for an "inherits" entry in the theme JSON.
|
||||
if( this.inherits ) {
|
||||
var cached = { };
|
||||
_.each( this.inherits, function(th, key) {
|
||||
var themesFolder = require.resolve('fresh-themes');
|
||||
var d = parsePath( themeFolder ).dirname;
|
||||
var themePath = PATH.join(d, th);
|
||||
cached[ th ] = cached[th] || new FRESHTheme().open( themePath );
|
||||
formatsHash[ key ] = cached[ th ].getFormat( key );
|
||||
});
|
||||
}
|
||||
|
||||
// Check for an explicit "formats" entry in the theme JSON. If it has one,
|
||||
// then this theme declares its files explicitly.
|
||||
if( !!this.formats ) {
|
||||
formatsHash = loadExplicit.call( this );
|
||||
formatsHash = loadExplicit.call( this, formatsHash );
|
||||
this.explicit = true;
|
||||
}
|
||||
else {
|
||||
formatsHash = loadImplicit.call( this );
|
||||
formatsHash = loadImplicit.call( this, formatsHash );
|
||||
}
|
||||
|
||||
// Cache
|
||||
@ -95,10 +107,9 @@ Definition of the FRESHTheme class.
|
||||
Load the theme implicitly, by scanning the theme folder for
|
||||
files. TODO: Refactor duplicated code with loadExplicit.
|
||||
*/
|
||||
function loadImplicit() {
|
||||
function loadImplicit(formatsHash) {
|
||||
|
||||
// Set up a hash of formats supported by this theme.
|
||||
var formatsHash = { };
|
||||
var that = this;
|
||||
var major = false;
|
||||
|
||||
@ -170,15 +181,26 @@ Definition of the FRESHTheme class.
|
||||
return fmt && (fmt.ext === 'css');
|
||||
}))
|
||||
|
||||
// For each CSS file, get its corresponding HTML file
|
||||
// For each CSS file, get its corresponding HTML file. It's possible that
|
||||
// a theme can have a CSS file but *no* HTML file, as when a theme author
|
||||
// creates a pure CSS override of an existing theme.
|
||||
.forEach(function( cssf ) {
|
||||
|
||||
var idx = _.findIndex(fmts, function( fmt ) {
|
||||
return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
|
||||
});
|
||||
cssf.action = null;
|
||||
fmts[ idx ].css = cssf.data;
|
||||
fmts[ idx ].cssPath = cssf.path;
|
||||
if( idx > -1) {
|
||||
fmts[ idx ].css = cssf.data;
|
||||
fmts[ idx ].cssPath = cssf.path;
|
||||
}
|
||||
else {
|
||||
if( that.inherits ) {
|
||||
// Found a CSS file without an HTML file in a theme that inherits
|
||||
// from another theme. This is the override CSS file.
|
||||
that.overrides = { file: cssf.path, data: cssf.data };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove CSS files from the formats array
|
||||
@ -195,10 +217,9 @@ Definition of the FRESHTheme class.
|
||||
Load the theme explicitly, by following the 'formats' hash
|
||||
in the theme's JSON settings file.
|
||||
*/
|
||||
function loadExplicit() {
|
||||
function loadExplicit(formatsHash) {
|
||||
|
||||
// Housekeeping
|
||||
var formatsHash = { };
|
||||
var tplFolder = PATH.join( this.folder, 'src' );
|
||||
var act = null;
|
||||
var that = this;
|
||||
|
@ -4,8 +4,12 @@ Definition of the JRSResume class.
|
||||
@module jrs-resume.js
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
|
||||
|
||||
var FS = require('fs')
|
||||
, extend = require('../utils/extend')
|
||||
, validator = require('is-my-json-valid')
|
||||
@ -15,25 +19,24 @@ Definition of the JRSResume class.
|
||||
, CONVERTER = require('./convert')
|
||||
, moment = require('moment');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
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.
|
||||
A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
|
||||
is an instantiation of that JSON decorated with utility methods.
|
||||
@class JRSResume
|
||||
*/
|
||||
function JRSResume() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
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.
|
||||
Initialize the JSResume from file.
|
||||
*/
|
||||
JRSResume.prototype.open = function( file, title ) {
|
||||
//this.imp = { fileName: file }; <-- schema violation, tuck it into .basics instead
|
||||
//this.imp = { fileName: file }; <-- schema violation, tuck it into .basics
|
||||
this.basics = {
|
||||
imp: {
|
||||
fileName: file,
|
||||
@ -43,15 +46,58 @@ Definition of the JRSResume class.
|
||||
return this.parse( this.basics.imp.raw, title );
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Initialize the the JSResume from string.
|
||||
*/
|
||||
JRSResume.prototype.parse = function( stringData, opts ) {
|
||||
opts = opts || { };
|
||||
var rep = JSON.parse( stringData );
|
||||
return this.parseJSON( rep, opts );
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Initialize the JRSResume from JSON.
|
||||
Open and parse the specified JRS resume. Merge the JSON object model onto this
|
||||
Sheet instance with extend() and convert sheet dates to a safe & consistent
|
||||
format. Then sort each section by startDate descending.
|
||||
*/
|
||||
JRSResume.prototype.parseJSON = function( rep, opts ) {
|
||||
opts = opts || { };
|
||||
extend( true, this, rep );
|
||||
// Set up metadata
|
||||
if( opts.imp === undefined || opts.imp ) {
|
||||
this.basics.imp = this.basics.imp || { };
|
||||
this.basics.imp.title =
|
||||
(opts.title || this.basics.imp.title) || this.basics.name;
|
||||
this.basics.imp.orgFormat = 'JRS';
|
||||
}
|
||||
// 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.basics.computed = {
|
||||
numYears: this.duration(),
|
||||
keywords: this.keywords()
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Save the sheet to disk (for environments that have disk access).
|
||||
*/
|
||||
JRSResume.prototype.save = function( filename ) {
|
||||
this.basics.imp.fileName = filename || this.basics.imp.fileName;
|
||||
FS.writeFileSync( this.basics.imp.fileName, this.stringify( this ), 'utf8' );
|
||||
FS.writeFileSync(this.basics.imp.fileName, this.stringify( this ), 'utf8');
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Save the sheet to disk in a specific format, either FRESH or JRS.
|
||||
*/
|
||||
@ -70,6 +116,17 @@ Definition of the JRSResume class.
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Return the resume format.
|
||||
*/
|
||||
JRSResume.prototype.format = function() {
|
||||
return 'JRS';
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Convert this object to a JSON string, sanitizing meta-properties along the
|
||||
way. Don't override .toString().
|
||||
@ -78,50 +135,20 @@ Definition of the JRSResume class.
|
||||
function replacer( key,value ) { // Exclude these keys from stringification
|
||||
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
|
||||
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
|
||||
'isModified', 'htmlPreview', 'display_progress_bar'],
|
||||
'isModified', 'htmlPreview', 'display_progress_bar'],
|
||||
function( val ) { return key.trim() === val; }
|
||||
) ? undefined : value;
|
||||
}
|
||||
return JSON.stringify( obj, replacer, 2 );
|
||||
};
|
||||
|
||||
|
||||
|
||||
JRSResume.prototype.stringify = function() {
|
||||
return JRSResume.stringify( this );
|
||||
};
|
||||
|
||||
/**
|
||||
Initialize the JRS Resume from string data.
|
||||
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.
|
||||
*/
|
||||
JRSResume.prototype.parse = function( stringData, opts ) {
|
||||
opts = opts || { };
|
||||
var rep = JSON.parse( stringData );
|
||||
return this.parseJSON( rep, opts );
|
||||
};
|
||||
|
||||
/**
|
||||
Initialize the JRSRume from JSON data.
|
||||
*/
|
||||
JRSResume.prototype.parseJSON = function( rep, opts ) {
|
||||
opts = opts || { };
|
||||
extend( true, this, rep );
|
||||
// Set up metadata
|
||||
if( opts.imp === undefined || opts.imp ) {
|
||||
this.basics.imp = this.basics.imp || { };
|
||||
this.basics.imp.title = (opts.title || this.basics.imp.title) || this.basics.name;
|
||||
this.basics.imp.orgFormat = 'JRS';
|
||||
}
|
||||
// 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.basics.computed = {
|
||||
numYears: this.duration(),
|
||||
keywords: this.keywords()
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
Return a unique list of all keywords across all skills.
|
||||
@ -136,6 +163,20 @@ Definition of the JRSResume class.
|
||||
return flatSkills;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Return internal metadata. Create if it doesn't exist.
|
||||
JSON Resume v0.0.0 doesn't allow additional properties at the root level,
|
||||
so tuck this into the .basic sub-object.
|
||||
*/
|
||||
JRSResume.prototype.i = function() {
|
||||
this.basics = this.basics || { imp: { } };
|
||||
return this.basics;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Reset the sheet to an empty state.
|
||||
*/
|
||||
@ -153,13 +194,19 @@ Definition of the JRSResume class.
|
||||
delete this.basics.profiles;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Get the default (empty) sheet.
|
||||
*/
|
||||
JRSResume.default = function() {
|
||||
return new JRSResume().open( PATH.join( __dirname, 'empty-jrs.json'), 'Empty' );
|
||||
return new JRSResume().open(
|
||||
PATH.join( __dirname, 'empty-jrs.json'), 'Empty'
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Add work experience to the sheet.
|
||||
*/
|
||||
@ -171,6 +218,8 @@ Definition of the JRSResume class.
|
||||
return newObject;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Determine if the sheet includes a specific social profile (eg, GitHub).
|
||||
*/
|
||||
@ -181,6 +230,8 @@ Definition of the JRSResume class.
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Determine if the sheet includes a specific skill.
|
||||
*/
|
||||
@ -193,11 +244,13 @@ Definition of the JRSResume class.
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Validate the sheet against the JSON Resume schema.
|
||||
*/
|
||||
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 validator = require('is-my-json-valid');
|
||||
var validate = validator( schemaObj, { // Note [1]
|
||||
@ -211,6 +264,8 @@ Definition of the JRSResume class.
|
||||
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().
|
||||
@ -233,6 +288,8 @@ Definition of the JRSResume class.
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Sort dated things on the sheet by start date descending. Assumes that dates
|
||||
on the sheet have been processed with _parseDates().
|
||||
@ -259,12 +316,16 @@ Definition of the JRSResume class.
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
JRSResume.prototype.dupe = function() {
|
||||
var rnew = new JRSResume();
|
||||
rnew.parse( this.stringify(), { } );
|
||||
return rnew;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Create a copy of this resume in which all fields have been interpreted as
|
||||
Markdown.
|
||||
@ -302,7 +363,9 @@ Definition of the JRSResume class.
|
||||
Object.keys( obj ).forEach(function(key) {
|
||||
var sub = obj[key];
|
||||
if( typeof sub === 'string' || sub instanceof String ) {
|
||||
if( _.contains(['skills','url','website','startDate','endDate','releaseDate','date','phone','email','address','postalCode','city','country','region'], key) )
|
||||
if( _.contains(['skills','url','website','startDate','endDate',
|
||||
'releaseDate','date','phone','email','address','postalCode',
|
||||
'city','country','region'], key) )
|
||||
return;
|
||||
if( key === 'summary' )
|
||||
obj[key] = HD( obj[key] );
|
||||
@ -323,6 +386,8 @@ Definition of the JRSResume class.
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
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
|
||||
@ -354,9 +419,13 @@ Definition of the JRSResume class.
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Export the JRSResume function/ctor.
|
||||
*/
|
||||
module.exports = JRSResume;
|
||||
|
||||
|
||||
|
||||
}());
|
||||
|
@ -13,6 +13,8 @@ Definition of the ResumeFactory class.
|
||||
require('string.prototype.startswith');
|
||||
var FS = require('fs');
|
||||
var ResumeConverter = require('./convert');
|
||||
var chalk = require('chalk');
|
||||
var SyntaxErrorEx = require('../utils/syntax-error-ex');
|
||||
|
||||
|
||||
|
||||
@ -27,12 +29,13 @@ Definition of the ResumeFactory class.
|
||||
/**
|
||||
Load one or more resumes from disk.
|
||||
*/
|
||||
load: function ( sources, log, toFormat, objectify ) {
|
||||
load: function ( sources, opts ) {
|
||||
|
||||
// Loop over all inputs, parsing each to JSON and then to a FRESHResume
|
||||
// or JRSResume object.
|
||||
var that = this;
|
||||
return sources.map( function( src ) {
|
||||
return that.loadOne( src, log, toFormat, objectify );
|
||||
return that.loadOne( src, opts );
|
||||
});
|
||||
|
||||
},
|
||||
@ -42,17 +45,21 @@ Definition of the ResumeFactory class.
|
||||
/**
|
||||
Load a single resume from disk.
|
||||
*/
|
||||
loadOne: function( src, log, toFormat, objectify ) {
|
||||
loadOne: function( src, opts ) {
|
||||
|
||||
var log = opts.log;
|
||||
var toFormat = opts.format;
|
||||
var objectify = opts.objectify;
|
||||
|
||||
// Get the destination format. Can be 'fresh', 'jrs', or null/undefined.
|
||||
toFormat && (toFormat = toFormat.toLowerCase().trim());
|
||||
|
||||
// Load and parse the resume JSON
|
||||
var info = _parse( src, log, toFormat );
|
||||
var info = _parse( src, opts );
|
||||
if( info.error ) return info;
|
||||
var json = info.json;
|
||||
|
||||
// Determine the resume format: FRESH or JRS
|
||||
var json = info.json;
|
||||
var orgFormat = ( json.meta && json.meta.format &&
|
||||
json.meta.format.startsWith('FRESH@') ) ?
|
||||
'fresh' : 'jrs';
|
||||
@ -68,6 +75,7 @@ Definition of the ResumeFactory class.
|
||||
if( objectify ) {
|
||||
var ResumeClass = require('../core/' + (toFormat || orgFormat) + '-resume');
|
||||
rez = new ResumeClass().parseJSON( json );
|
||||
rez.i().file = src;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -80,25 +88,41 @@ Definition of the ResumeFactory class.
|
||||
|
||||
|
||||
|
||||
function _parse( fileName, log, toFormat ) {
|
||||
function _parse( fileName, opts ) {
|
||||
var rawData;
|
||||
try {
|
||||
|
||||
// TODO: Core should not log
|
||||
log( 'Reading '.info + /*orgFormat.toUpperCase().infoBold +*/
|
||||
'resume: '.info + fileName.cyan.bold );
|
||||
opts.log( chalk.cyan('Reading resume: ') + chalk.cyan.bold(fileName) );
|
||||
|
||||
// Read the file
|
||||
rawData = FS.readFileSync( fileName, 'utf8' );
|
||||
|
||||
// Parse it to JSON
|
||||
return {
|
||||
json: JSON.parse( rawData )
|
||||
};
|
||||
|
||||
}
|
||||
catch(ex) {
|
||||
return {
|
||||
catch( ex ) {
|
||||
|
||||
// JSON.parse failed due to invalid JSON
|
||||
if ( !opts.muffle && ex instanceof SyntaxError) {
|
||||
var info = new SyntaxErrorEx( ex, rawData );
|
||||
opts.log( chalk.red.bold(fileName.toUpperCase() + ' contains invalid JSON on line ' +
|
||||
info.line + ' column ' + info.col + '.' +
|
||||
chalk.red(' Unable to validate.')));
|
||||
opts.log( chalk.red.bold('INTERNAL: ' + ex) );
|
||||
ex.handled = true;
|
||||
}
|
||||
|
||||
if( opts.throw ) throw ex;
|
||||
else return {
|
||||
error: ex,
|
||||
raw: rawData
|
||||
raw: rawData,
|
||||
file: fileName
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,8 @@ Status codes for HackMyResume.
|
||||
inputOutputParity: 7,
|
||||
createNameMissing: 8,
|
||||
wkhtmltopdf: 9,
|
||||
missingPackageJSON: 10
|
||||
missingPackageJSON: 10,
|
||||
invalid: 11
|
||||
};
|
||||
|
||||
}());
|
||||
|
@ -11,7 +11,9 @@ Generic template helper definitions for HackMyResume / FluentCV.
|
||||
, H2W = require('../utils/html-to-wpml')
|
||||
, XML = require('xml-escape')
|
||||
, moment = require('moment')
|
||||
, _ = require('underscore');
|
||||
, LO = require('lodash')
|
||||
, _ = require('underscore')
|
||||
, unused = require('../utils/string');
|
||||
|
||||
/**
|
||||
Generic template helper function definitions.
|
||||
@ -21,10 +23,109 @@ Generic template helper definitions for HackMyResume / FluentCV.
|
||||
|
||||
/**
|
||||
Convert the input date to a specified format through Moment.js.
|
||||
If date is invalid, will return the time provided by the user,
|
||||
or default to the fallback param or 'Present' if that is set to true
|
||||
@method formatDate
|
||||
*/
|
||||
formatDate: function(datetime, format) {
|
||||
return moment ? moment( datetime ).format( format ) : datetime;
|
||||
formatDate: function(datetime, format, fallback) {
|
||||
if (moment) {
|
||||
var momentDate = moment( datetime );
|
||||
if (momentDate.isValid()) return momentDate.format(format);
|
||||
}
|
||||
|
||||
return datetime || (typeof fallback == 'string' ? fallback : (fallback === true ? 'Present' : null));
|
||||
},
|
||||
|
||||
/**
|
||||
Format a from/to date range.
|
||||
@method dateRange
|
||||
*/
|
||||
dateRange: function( obj, fmt, sep, options ) {
|
||||
fmt = (fmt && String.is(fmt) && fmt) || 'YYYY-MM';
|
||||
sep = (sep && String.is(sep) && sep) || ' — ';
|
||||
if( obj.safe ) {
|
||||
var dateA = (obj.safe.start && obj.safe.start.format(fmt)) || '';
|
||||
var dateB = (obj.safe.end && obj.safe.end.format(fmt)) || '';
|
||||
if( obj.safe.start && obj.safe.end ) {
|
||||
return dateA + sep + dateB ;
|
||||
}
|
||||
else if( obj.safe.start || obj.safe.end ) {
|
||||
return dateA || dateB;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
Return true if the section is present on the resume and has at least one
|
||||
element.
|
||||
@method section
|
||||
*/
|
||||
section: function( title, options ) {
|
||||
title = title.trim().toLowerCase();
|
||||
var obj = LO.get( this.r, title );
|
||||
if( _.isArray( obj ) ) {
|
||||
return obj.length ? options.fn(this) : undefined;
|
||||
}
|
||||
else if( _.isObject( obj )) {
|
||||
return ( (obj.history && obj.history.length) ||
|
||||
( obj.sets && obj.sets.length ) ) ?
|
||||
options.fn(this) : undefined;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Capitalize the first letter of the word.
|
||||
@method section
|
||||
*/
|
||||
camelCase: function(val) {
|
||||
val = (val && val.trim()) || '';
|
||||
return val ? (val.charAt(0).toUpperCase() + val.slice(1)) : val;
|
||||
},
|
||||
|
||||
/**
|
||||
Return true if the context has the property or subpropery.
|
||||
@method has
|
||||
*/
|
||||
has: function( title, options ) {
|
||||
title = title && title.trim().toLowerCase();
|
||||
if( LO.get( this.r, title ) ) {
|
||||
return options.fn(this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Generic template helper function to display a user-overridable section
|
||||
title for a FRESH resume theme. Use this in lieue of hard-coding section
|
||||
titles.
|
||||
|
||||
Usage:
|
||||
|
||||
{{sectionTitle "sectionName"}}
|
||||
{{sectionTitle "sectionName" "sectionTitle"}}
|
||||
|
||||
Example:
|
||||
|
||||
{{sectionTitle "Education"}}
|
||||
{{sectionTitle "Employment" "Project History"}}
|
||||
|
||||
@param sect_name The name of the section being title. Must be one of the
|
||||
top-level FRESH resume sections ("info", "education", "employment", etc.).
|
||||
@param sect_title The theme-specified section title. May be replaced by the
|
||||
user.
|
||||
@method sectionTitle
|
||||
*/
|
||||
sectionTitle: function( sname, stitle ) {
|
||||
|
||||
// If not provided by the user, stitle should default to sname. ps.
|
||||
// Handlebars silently passes in the options object to the last param,
|
||||
// where in Underscore stitle will be null/undefined, so we check both.
|
||||
stitle = (stitle && String.is(stitle) && stitle) || sname;
|
||||
|
||||
// If there's a section title override, use it.
|
||||
return ( this.opts.stitles &&
|
||||
this.opts.stitles[ sname.toLowerCase().trim() ] ) ||
|
||||
stitle;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -39,7 +140,6 @@ Generic template helper definitions for HackMyResume / FluentCV.
|
||||
MD(txt).replace(/^\s*<p>|<\/p>\s*$/gi, '') :
|
||||
MD(txt);
|
||||
txt = H2W( txt );
|
||||
console.log(txt);
|
||||
return txt;
|
||||
},
|
||||
|
||||
@ -61,7 +161,7 @@ Generic template helper definitions for HackMyResume / FluentCV.
|
||||
},
|
||||
|
||||
/**
|
||||
Convert a skill level to an RGB color triplet.
|
||||
Convert a skill level to an RGB color triplet. TODO: refactor
|
||||
@method skillColor
|
||||
@param lvl Input skill level. Skill level can be expressed as a string
|
||||
("beginner", "intermediate", etc.), as an integer (1,5,etc), as a string
|
||||
@ -77,7 +177,7 @@ Generic template helper definitions for HackMyResume / FluentCV.
|
||||
},
|
||||
|
||||
/**
|
||||
Return an appropriate height.
|
||||
Return an appropriate height. TODO: refactor
|
||||
@method lastWord
|
||||
*/
|
||||
skillHeight: function( lvl ) {
|
||||
@ -122,9 +222,17 @@ Generic template helper definitions for HackMyResume / FluentCV.
|
||||
via <style></style> tag.
|
||||
*/
|
||||
styleSheet: function( file, options ) {
|
||||
return ( this.opts.css === 'link') ?
|
||||
var styles = ( this.opts.css === 'link') ?
|
||||
'<link href="' + file + '" rel="stylesheet" type="text/css">' :
|
||||
'<style>' + this.cssInfo.data + '</style>';
|
||||
if( this.opts.themeObj.inherits &&
|
||||
this.opts.themeObj.inherits.html &&
|
||||
this.format === 'html' ) {
|
||||
styles += (this.opts.css === 'link') ?
|
||||
'<link href="' + this.opts.themeObj.overrides.path + '" rel="stylesheet" type="text/css">' :
|
||||
'<style>' + this.opts.themeObj.overrides.data + '</style>';
|
||||
}
|
||||
return styles;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1,9 +1,11 @@
|
||||
/**
|
||||
Definition of the HandlebarsGenerator class.
|
||||
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
|
||||
@license MIT. See LICENSE.md for details.
|
||||
@module handlebars-generator.js
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
|
||||
@ -11,7 +13,11 @@ Definition of the HandlebarsGenerator class.
|
||||
var _ = require('underscore')
|
||||
, HANDLEBARS = require('handlebars')
|
||||
, FS = require('fs')
|
||||
, registerHelpers = require('./handlebars-helpers');
|
||||
, registerHelpers = require('./handlebars-helpers')
|
||||
, PATH = require('path')
|
||||
, parsePath = require('parse-filepath')
|
||||
, READFILES = require('recursive-readdir-sync')
|
||||
, SLASH = require('slash');
|
||||
|
||||
|
||||
|
||||
@ -21,36 +27,73 @@ Definition of the HandlebarsGenerator class.
|
||||
*/
|
||||
var HandlebarsGenerator = module.exports = {
|
||||
|
||||
|
||||
|
||||
|
||||
generate: function( json, jst, format, cssInfo, opts, theme ) {
|
||||
|
||||
// Pre-compile any partials present in the theme.
|
||||
_.each( theme.partials, function( el ) {
|
||||
var tplData = FS.readFileSync( el.path, 'utf8' );
|
||||
var compiledTemplate = HANDLEBARS.compile( tplData );
|
||||
HANDLEBARS.registerPartial( el.name, compiledTemplate );
|
||||
});
|
||||
|
||||
// Register necessary helpers.
|
||||
registerPartials( format, theme );
|
||||
registerHelpers( theme );
|
||||
|
||||
// Compile and run the Handlebars template.
|
||||
var template = HANDLEBARS.compile(jst);
|
||||
|
||||
// Preprocess text
|
||||
var encData = json;
|
||||
( format === 'html' || format === 'pdf' ) && (encData = json.markdownify());
|
||||
( format === 'doc' ) && (encData = json.xmlify());
|
||||
|
||||
// Compile and run the Handlebars template.
|
||||
var template = HANDLEBARS.compile(jst);
|
||||
return template({
|
||||
r: encData,
|
||||
RAW: json,
|
||||
filt: opts.filters,
|
||||
cssInfo: cssInfo,
|
||||
format: format,
|
||||
opts: opts,
|
||||
headFragment: opts.headFragment || ''
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
function registerPartials(format, theme) {
|
||||
if( format !== 'html' && format != 'doc' )
|
||||
return;
|
||||
|
||||
// Locate the global partials folder
|
||||
var partialsFolder = PATH.join(
|
||||
parsePath( require.resolve('fresh-themes') ).dirname,
|
||||
'/partials/',
|
||||
format
|
||||
);
|
||||
|
||||
// Register global partials in the /partials folder
|
||||
// TODO: Only do this once per HMR invocation.
|
||||
_.each( READFILES( partialsFolder, function(error){ }), function( el ) {
|
||||
var pathInfo = parsePath( el );
|
||||
var name = SLASH( PATH.relative( partialsFolder, el )
|
||||
.replace(/\.html$|\.xml$/, '') );
|
||||
if( pathInfo.dirname.endsWith('section') ) {
|
||||
name = SLASH(name.replace(/\.html$|\.xml$/, ''));
|
||||
}
|
||||
var tplData = FS.readFileSync( el, 'utf8' );
|
||||
var compiledTemplate = HANDLEBARS.compile( tplData );
|
||||
HANDLEBARS.registerPartial( name, compiledTemplate );
|
||||
theme.partialsInitialized = true;
|
||||
});
|
||||
|
||||
// Register theme-specific partials
|
||||
_.each( theme.partials, function( el ) {
|
||||
var tplData = FS.readFileSync( el.path, 'utf8' );
|
||||
var compiledTemplate = HANDLEBARS.compile( tplData );
|
||||
HANDLEBARS.registerPartial( el.name, compiledTemplate );
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}());
|
||||
|
@ -1,6 +1,7 @@
|
||||
/**
|
||||
Definition of the HtmlPdfGenerator class.
|
||||
@module html-pdf-generator.js
|
||||
@license MIT. See LICENSE.md for details.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
@ -10,7 +11,7 @@ Definition of the HtmlPdfGenerator class.
|
||||
, HTML = require( 'html' );
|
||||
|
||||
/**
|
||||
An HTML-based PDF resume generator for HackMyResume.
|
||||
An HTML-driven PDF resume generator for HackMyResume.
|
||||
*/
|
||||
var HtmlPdfGenerator = module.exports = TemplateGenerator.extend({
|
||||
|
||||
@ -22,61 +23,59 @@ Definition of the HtmlPdfGenerator class.
|
||||
Generate the binary PDF.
|
||||
*/
|
||||
onBeforeSave: function( info ) {
|
||||
pdf.call( this, info.mk, info.outputFile );
|
||||
engines[ info.opts.pdf || 'wkhtmltopdf' ]
|
||||
.call( this, info.mk, info.outputFile );
|
||||
return null; // halt further processing
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
Generate a PDF from HTML.
|
||||
*/
|
||||
function pdf( markup, fOut ) {
|
||||
|
||||
pdf_wkhtmltopdf.call( this, markup, fOut );
|
||||
var engines = {
|
||||
/**
|
||||
Generate a PDF from HTML using wkhtmltopdf.
|
||||
*/
|
||||
wkhtmltopdf: function(markup, fOut) {
|
||||
var wk;
|
||||
try {
|
||||
wk = require('wkhtmltopdf');
|
||||
wk( markup, { pageSize: 'letter' } )
|
||||
.pipe( FS.createWriteStream( fOut ) );
|
||||
}
|
||||
catch(ex) {
|
||||
// { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', ... }
|
||||
// { [Error: ENOENT] }
|
||||
throw { fluenterror: this.codes.wkhtmltopdf };
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Generate a PDF from HTML using wkhtmltopdf.
|
||||
*/
|
||||
function pdf_wkhtmltopdf( markup, fOut ) {
|
||||
var wk;
|
||||
try {
|
||||
wk = require('wkhtmltopdf');
|
||||
wk( markup, { pageSize: 'letter' } )
|
||||
.pipe( FS.createWriteStream( fOut ) );
|
||||
/**
|
||||
Generate a PDF from HTML using Phantom.
|
||||
*/
|
||||
phantom: function( markup, fOut ) {
|
||||
require('phantom').create( function( ph ) {
|
||||
ph.createPage( function( page ) {
|
||||
page.setContent( markup );
|
||||
page.set('paperSize', {
|
||||
format: 'A4',
|
||||
orientation: 'portrait',
|
||||
margin: '1cm'
|
||||
});
|
||||
page.set("viewportSize", {
|
||||
width: 1024, // TODO: option-ify
|
||||
height: 768 // TODO: Use "A" sizes
|
||||
});
|
||||
page.set('onLoadFinished', function(success) {
|
||||
page.render( fOut );
|
||||
ph.exit();
|
||||
});
|
||||
},
|
||||
{ dnodeOpts: { weak: false } } );
|
||||
});
|
||||
}
|
||||
catch(ex) {
|
||||
// { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', syscall: 'write' }
|
||||
// { [Error: ENOENT] }
|
||||
throw { fluenterror: this.codes.wkhtmltopdf };
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
// function pdf_phantom() {
|
||||
// pdfCount++;
|
||||
// require('phantom').create( function( ph ) {
|
||||
// ph.createPage( function( page ) {
|
||||
// page.setContent( markup );
|
||||
// page.set('paperSize', {
|
||||
// format: 'A4',
|
||||
// orientation: 'portrait',
|
||||
// margin: '1cm'
|
||||
// });
|
||||
// page.set("viewportSize", {
|
||||
// width: 1024, // TODO: option-ify
|
||||
// height: 768 // TODO: Use "A" sizes
|
||||
// });
|
||||
// page.set('onLoadFinished', function(success) {
|
||||
// page.render( fOut );
|
||||
// pdfCount++;
|
||||
// ph.exit();
|
||||
// });
|
||||
// },
|
||||
// { dnodeOpts: { weak: false } } );
|
||||
// });
|
||||
// }
|
||||
|
||||
}());
|
||||
|
@ -152,7 +152,8 @@ Definition of the TemplateGenerator class.
|
||||
file.data = that.onBeforeSave({
|
||||
theme: theme,
|
||||
outputFile: (file.info.major ? f : thisFilePath),
|
||||
mk: file.data
|
||||
mk: file.data,
|
||||
opts: that.opts
|
||||
});
|
||||
if( !file.data ) return; // PDF etc
|
||||
}
|
||||
@ -161,7 +162,7 @@ Definition of the TemplateGenerator class.
|
||||
FS.writeFileSync( fileName, file.data,
|
||||
{ encoding: 'utf8', flags: 'w' } );
|
||||
that.onAfterSave && that.onAfterSave(
|
||||
{ outputFile: fileName, mk: file.data } );
|
||||
{ outputFile: fileName, mk: file.data, opts: that.opts } );
|
||||
}
|
||||
catch(ex) {
|
||||
require('../core/error-handler').err(ex, false);
|
||||
@ -234,12 +235,12 @@ Definition of the TemplateGenerator class.
|
||||
// Verify the specified theme name/path
|
||||
var tFolder = PATH.join(
|
||||
parsePath( require.resolve('fresh-themes') ).dirname,
|
||||
'/themes/',
|
||||
this.opts.theme
|
||||
);
|
||||
|
||||
var t;
|
||||
if( this.opts.theme.startsWith('jsonresume-theme-') ) {
|
||||
console.log('LOADING JSON RESUME');
|
||||
t = new JRSTheme().open( tFolder );
|
||||
}
|
||||
else {
|
||||
@ -274,6 +275,7 @@ Definition of the TemplateGenerator class.
|
||||
}
|
||||
catch(ex) {
|
||||
console.log(ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,21 +4,40 @@ External API surface for HackMyResume.
|
||||
@module hackmyapi.js
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
Sheet: require('./core/fresh-resume'),
|
||||
FRESHResume: require('./core/fresh-resume'),
|
||||
JRSResume: require('./core/jrs-resume'),
|
||||
FRESHTheme: require('./core/fresh-theme'),
|
||||
JRSTheme: require('./core/jrs-theme'),
|
||||
FluentDate: require('./core/fluent-date'),
|
||||
HtmlGenerator: require('./gen/html-generator'),
|
||||
TextGenerator: require('./gen/text-generator'),
|
||||
HtmlPdfGenerator: require('./gen/html-pdf-generator'),
|
||||
WordGenerator: require('./gen/word-generator'),
|
||||
MarkdownGenerator: require('./gen/markdown-generator'),
|
||||
JsonGenerator: require('./gen/json-generator'),
|
||||
YamlGenerator: require('./gen/yaml-generator'),
|
||||
JsonYamlGenerator: require('./gen/json-yaml-generator'),
|
||||
LaTeXGenerator: require('./gen/latex-generator'),
|
||||
HtmlPngGenerator: require('./gen/html-png-generator')
|
||||
};
|
||||
(function() {
|
||||
|
||||
var v = {
|
||||
build: require('./verbs/generate'),
|
||||
analyze: require('./verbs/analyze'),
|
||||
validate: require('./verbs/validate'),
|
||||
convert: require('./verbs/convert'),
|
||||
new: require('./verbs/create')
|
||||
};
|
||||
|
||||
var HackMyAPI = module.exports = {
|
||||
verbs: v,
|
||||
alias: {
|
||||
generate: v.build,
|
||||
create: v.new
|
||||
},
|
||||
options: require('./core/default-options'),
|
||||
formats: require('./core/default-formats'),
|
||||
Sheet: require('./core/fresh-resume'),
|
||||
FRESHResume: require('./core/fresh-resume'),
|
||||
JRSResume: require('./core/jrs-resume'),
|
||||
FRESHTheme: require('./core/fresh-theme'),
|
||||
JRSTheme: require('./core/jrs-theme'),
|
||||
FluentDate: require('./core/fluent-date'),
|
||||
HtmlGenerator: require('./gen/html-generator'),
|
||||
TextGenerator: require('./gen/text-generator'),
|
||||
HtmlPdfGenerator: require('./gen/html-pdf-generator'),
|
||||
WordGenerator: require('./gen/word-generator'),
|
||||
MarkdownGenerator: require('./gen/markdown-generator'),
|
||||
JsonGenerator: require('./gen/json-generator'),
|
||||
YamlGenerator: require('./gen/yaml-generator'),
|
||||
JsonYamlGenerator: require('./gen/json-yaml-generator'),
|
||||
LaTeXGenerator: require('./gen/latex-generator'),
|
||||
HtmlPngGenerator: require('./gen/html-png-generator')
|
||||
};
|
||||
|
||||
}());
|
||||
|
@ -1,50 +0,0 @@
|
||||
/**
|
||||
Internal resume generation logic for HackMyResume.
|
||||
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
|
||||
@module hackmycmd.js
|
||||
*/
|
||||
|
||||
(function() {
|
||||
module.exports = function () {
|
||||
|
||||
var unused = require('./utils/string')
|
||||
, PATH = require('path')
|
||||
, FS = require('fs');
|
||||
|
||||
|
||||
/**
|
||||
Display help documentation.
|
||||
*/
|
||||
function help() {
|
||||
var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' );
|
||||
console.log( manPage.useful.bold );
|
||||
}
|
||||
|
||||
/**
|
||||
Internal module interface. Used by FCV Desktop and HMR.
|
||||
*/
|
||||
var v = {
|
||||
build: require('./verbs/generate'),
|
||||
validate: require('./verbs/validate'),
|
||||
convert: require('./verbs/convert'),
|
||||
new: require('./verbs/create'),
|
||||
help: help
|
||||
};
|
||||
|
||||
return {
|
||||
verbs: v,
|
||||
alias: {
|
||||
generate: v.build,
|
||||
create: v.build
|
||||
},
|
||||
lib: require('./hackmyapi'),
|
||||
options: require('./core/default-options'),
|
||||
formats: require('./core/default-formats')
|
||||
};
|
||||
|
||||
}();
|
||||
|
||||
}());
|
||||
|
||||
// [1]: JSON.parse throws SyntaxError on invalid JSON. See:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
|
258
src/index.js
258
src/index.js
@ -1,5 +1,7 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Command-line interface (CLI) for HackMyResume.
|
||||
@license MIT. Copyright (c) 2015 hacksalot (https://github.com/hacksalot)
|
||||
@ -9,17 +11,19 @@ Command-line interface (CLI) for HackMyResume.
|
||||
|
||||
|
||||
var SPAWNW = require('./core/spawn-watch')
|
||||
, ARGS = require( 'minimist' )
|
||||
, FCMD = require( './hackmycmd')
|
||||
, HMR = require( './hackmyapi')
|
||||
, PKG = require('../package.json')
|
||||
, COLORS = require('colors')
|
||||
, FS = require('fs')
|
||||
, EXTEND = require('./utils/extend')
|
||||
, chalk = require('chalk')
|
||||
, PATH = require('path')
|
||||
, HACKMYSTATUS = require('./core/status-codes')
|
||||
, opts = { }
|
||||
, title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white
|
||||
, _ = require('underscore');
|
||||
|
||||
, safeLoadJSON = require('./utils/safe-json-loader')
|
||||
, _opts = { }
|
||||
, title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***')
|
||||
, StringUtils = require('./utils/string.js')
|
||||
, _ = require('underscore')
|
||||
, Command = require('commander').Command;
|
||||
|
||||
|
||||
|
||||
@ -31,67 +35,207 @@ catch( ex ) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Kick off the HackMyResume application.
|
||||
*/
|
||||
function main() {
|
||||
|
||||
// Colorize
|
||||
COLORS.setTheme({
|
||||
title: ['white','bold'],
|
||||
info: process.platform === 'win32' ? 'gray' : ['white','dim'],
|
||||
infoBold: ['white','dim'],
|
||||
warn: 'yellow',
|
||||
error: 'red',
|
||||
guide: 'yellow',
|
||||
status: 'gray',//['white','dim'],
|
||||
useful: 'green',
|
||||
});
|
||||
var args = initialize();
|
||||
|
||||
// Create the top-level (application) command...
|
||||
var program = new Command('hackmyresume')
|
||||
.version(PKG.version)
|
||||
.description(chalk.yellow.bold('*** HackMyResume ***'))
|
||||
.option('-o --opts <optionsFile>', 'Path to a .hackmyrc options file')
|
||||
.option('-s --silent', 'Run in silent mode')
|
||||
.option('--no-color', 'Disable colors')
|
||||
.option('--color', 'Enable colors');
|
||||
//.usage('COMMAND <sources> [TO <targets>]');
|
||||
|
||||
// Create the NEW command
|
||||
program
|
||||
.command('new')
|
||||
.arguments('<sources...>')
|
||||
.option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
|
||||
.alias('create')
|
||||
.description('Create resume(s) in FRESH or JSON RESUME format.')
|
||||
.action(function( sources ) {
|
||||
execVerb.call( this, sources, [], this.opts(), logMsg);
|
||||
});
|
||||
|
||||
// Create the VALIDATE command
|
||||
program
|
||||
.command('validate')
|
||||
.arguments('<sources...>')
|
||||
.option('-a --assert', 'Treat validation warnings as errors', false)
|
||||
.description('Validate a resume in FRESH or JSON RESUME format.')
|
||||
.action(function(sources) {
|
||||
execVerb.call( this, sources, [], this.opts(), logMsg);
|
||||
});
|
||||
|
||||
// Create the CONVERT command
|
||||
program
|
||||
.command('convert')
|
||||
//.arguments('<sources...>')
|
||||
.description('Convert a resume to/from FRESH or JSON RESUME format.')
|
||||
.action(function() {
|
||||
var x = splitSrcDest.call( this );
|
||||
execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
|
||||
});
|
||||
|
||||
// Create the ANALYZE command
|
||||
program
|
||||
.command('analyze')
|
||||
.arguments('<sources...>')
|
||||
.description('Analyze one or more resumes.')
|
||||
.action(function( sources ) {
|
||||
execVerb.call( this, sources, [], this.opts(), logMsg);
|
||||
});
|
||||
|
||||
// Create the BUILD command
|
||||
program
|
||||
.command('build')
|
||||
.alias('generate')
|
||||
//.arguments('<sources> TO [targets]')
|
||||
//.usage('...')
|
||||
.option('-t --theme <theme>', 'Theme name or path')
|
||||
.option('-n --no-prettify', 'Disable HTML prettification', true)
|
||||
.option('-c --css <option>', 'CSS linking / embedding', 'embed')
|
||||
.option('-p --pdf <engine>', 'PDF generation engine')
|
||||
.option('--no-tips', 'Disable theme tips and warnings.', false)
|
||||
.description('Generate resume to multiple formats')
|
||||
.action(function( sources, targets, options ) {
|
||||
var x = splitSrcDest.call( this );
|
||||
execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
|
||||
});
|
||||
|
||||
// program.on('--help', function(){
|
||||
// console.log(' Examples:');
|
||||
// console.log('');
|
||||
// console.log(' $ custom-help --help');
|
||||
// console.log(' $ custom-help -h');
|
||||
// console.log('');
|
||||
// });
|
||||
|
||||
program.parse( args );
|
||||
|
||||
if (!program.args.length) { throw { fluenterror: 4 }; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Massage command-line args and setup Commander.js.
|
||||
*/
|
||||
function initialize() {
|
||||
|
||||
// Setup
|
||||
if( process.argv.length <= 2 ) { throw { fluenterror: 4 }; }
|
||||
var a = ARGS( process.argv.slice(2) );
|
||||
opts = getOpts( a );
|
||||
logMsg( title );
|
||||
|
||||
// Get the action to be performed
|
||||
var params = a._.map( function(p){ return p.toLowerCase().trim(); });
|
||||
var verb = params[0];
|
||||
if( !FCMD.verbs[ verb ] && !FCMD.alias[ verb ] ) {
|
||||
logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn);
|
||||
return;
|
||||
// Support case-insensitive sub-commands (build, generate, validate, etc.)..
|
||||
var oVerb, verb = '', args = process.argv.slice(), cleanArgs = args.slice(2);
|
||||
if( cleanArgs.length ) {
|
||||
var verbIdx = _.findIndex( cleanArgs, function(v){ return v[0] !== '-'; });
|
||||
if( verbIdx !== -1 ) {
|
||||
oVerb = cleanArgs[ verbIdx ];
|
||||
verb = args[ verbIdx + 2 ] = oVerb.trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle invalid verbs here (a bit easier here than in commander.js)...
|
||||
if( verb && !HMR.verbs[ verb ] && !HMR.alias[ verb ] ) {
|
||||
throw { fluenterror: HACKMYSTATUS.invalidCommand, shouldExit: true,
|
||||
attempted: oVerb };
|
||||
}
|
||||
|
||||
// Override the .missingArgument behavior
|
||||
Command.prototype.missingArgument = function(name) {
|
||||
throw { fluenterror: HACKMYSTATUS.resumeNotFound };
|
||||
};
|
||||
|
||||
// Override the .helpInformation behavior
|
||||
Command.prototype.helpInformation = function() {
|
||||
var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' );
|
||||
return chalk.green.bold(manPage);
|
||||
};
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Invoke a HackMyResume verb.
|
||||
*/
|
||||
function execVerb( src, dst, opts, log ) {
|
||||
loadOptions.call( this, opts );
|
||||
HMR.verbs[ this.name() ].call( null, src, dst, _opts, log );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Initialize HackMyResume options.
|
||||
*/
|
||||
function loadOptions( opts ) {
|
||||
|
||||
opts.opts = this.parent.opts;
|
||||
|
||||
// Load the specified options file (if any) and apply options
|
||||
if( opts.opts && String.is( opts.opts )) {
|
||||
var json = safeLoadJSON( PATH.relative( process.cwd(), opts.opts ) );
|
||||
json && ( opts = EXTEND( true, opts, json ) );
|
||||
if( !json ) {
|
||||
throw safeLoadJSON.error;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge in command-line options
|
||||
opts = EXTEND( true, opts, this.opts() );
|
||||
opts.silent = this.parent.silent;
|
||||
_opts = opts;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Split multiple command-line filenames by the 'TO' keyword
|
||||
*/
|
||||
function splitSrcDest() {
|
||||
|
||||
var params = this.parent.args.filter(function(j) { return String.is(j); });
|
||||
if( params.length === 0 )
|
||||
throw { fluenterror: HACKMYSTATUS.resumeNotFound };
|
||||
|
||||
// Find the TO keyword, if any
|
||||
var splitAt = _.indexOf( params, 'to' );
|
||||
if( splitAt === a._.length - 1 ) {
|
||||
// 'TO' cannot be the last argument
|
||||
logMsg('Please '.warn + 'specify an output file'.warn.bold +
|
||||
' for this operation or '.warn + 'omit the TO keyword'.warn.bold +
|
||||
'.'.warn );
|
||||
var splitAt = _.findIndex( params, function(p) {
|
||||
return p.toLowerCase() === 'to';
|
||||
});
|
||||
|
||||
// TO can't be the last keyword
|
||||
if( splitAt === params.length - 1 && splitAt !== -1 ) {
|
||||
logMsg(chalk.yellow('Please ') +
|
||||
chalk.yellow.bold('specify an output file') +
|
||||
chalk.yellow(' for this operation or ') +
|
||||
chalk.yellow.bold('omit the TO keyword') +
|
||||
chalk.yellow('.') );
|
||||
return;
|
||||
}
|
||||
|
||||
// Massage inputs and outputs
|
||||
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
|
||||
var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 );
|
||||
( splitAt === -1 ) && (src.length > 1) && (verb !== 'validate') && dst.push( src.pop() ); // Allow omitting TO keyword
|
||||
|
||||
// Invoke the action
|
||||
(FCMD.verbs[verb] || FCMD.alias[verb]).apply(null, [src, dst, opts, logMsg]);
|
||||
|
||||
}
|
||||
|
||||
function logMsg( msg ) {
|
||||
opts.silent || console.log( msg );
|
||||
}
|
||||
|
||||
function getOpts( args ) {
|
||||
var noPretty = args.nopretty || args.n;
|
||||
noPretty = noPretty && (noPretty === true || noPretty === 'true');
|
||||
return {
|
||||
theme: args.t || 'modern',
|
||||
format: args.f || 'FRESH',
|
||||
prettify: !noPretty,
|
||||
silent: args.s || args.silent,
|
||||
css: args.css || 'embed'
|
||||
src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
|
||||
dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Simple logging placeholder.
|
||||
*/
|
||||
function logMsg( msg ) {
|
||||
msg = msg || '';
|
||||
_opts.silent || console.log( msg );
|
||||
}
|
||||
|
151
src/inspectors/gap-inspector.js
Normal file
151
src/inspectors/gap-inspector.js
Normal file
@ -0,0 +1,151 @@
|
||||
/**
|
||||
Employment gap analysis for HackMyResume.
|
||||
@license MIT. See LICENSE.md for details.
|
||||
@module gap-inspector.js
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
|
||||
|
||||
var _ = require('underscore');
|
||||
var FluentDate = require('../core/fluent-date');
|
||||
var moment = require('moment');
|
||||
var LO = require('lodash');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Identify gaps in the candidate's employment history.
|
||||
@class gapInspector
|
||||
*/
|
||||
var gapInspector = module.exports = {
|
||||
|
||||
|
||||
|
||||
moniker: 'gap-inspector',
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Run the Gap Analyzer on a resume.
|
||||
@method run
|
||||
@return An array of object representing gaps in the candidate's employment
|
||||
history. Each object provides the start, end, and duration of the gap:
|
||||
{ <-- gap
|
||||
start: // A Moment.js date
|
||||
end: // A Moment.js date
|
||||
duration: // Gap length
|
||||
}
|
||||
*/
|
||||
run: function( rez ) {
|
||||
|
||||
// This is what we'll return
|
||||
var coverage = {
|
||||
gaps: [],
|
||||
overlaps: [],
|
||||
duration: {
|
||||
total: 0,
|
||||
work: 0,
|
||||
gaps: 0
|
||||
},
|
||||
pct: '0%'
|
||||
};
|
||||
|
||||
// Missing employment section? Bye bye.
|
||||
var hist = LO.get( rez, 'employment.history' );
|
||||
if( !hist || !hist.length ) { return coverage; }
|
||||
|
||||
// Convert the candidate's employment history to an array of dates,
|
||||
// where each element in the array is a start date or an end date of a
|
||||
// job -- it doesn't matter which.
|
||||
var new_e = hist.map( function( job ){
|
||||
var obj = _.pick( job, ['start', 'end'] );
|
||||
if( obj && (obj.start || obj.end)) {
|
||||
obj = _.pairs( obj );
|
||||
obj[0][1] = FluentDate.fmt( obj[0][1] );
|
||||
if( obj.length > 1 )
|
||||
obj[1][1] = FluentDate.fmt( obj[1][1] );
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
|
||||
|
||||
// Flatten the array, remove empties, and sort
|
||||
new_e = _.filter( _.flatten( new_e, true ), function(v) {
|
||||
return ( v && v.length && v[0] && v[0].length );
|
||||
});
|
||||
if( !new_e || !new_e.length ) return coverage;
|
||||
new_e = _.sortBy( new_e, function( elem ) { return elem[1].unix(); });
|
||||
|
||||
// Iterate over elements in the array. Each time a start date is found,
|
||||
// increment a reference count. Each time an end date is found, decrement
|
||||
// the reference count. When the reference count reaches 0, we have a gap.
|
||||
// When the reference count is > 0, the candidate is employed. When the
|
||||
// reference count reaches 2, the candidate is overlapped.
|
||||
|
||||
var num_gaps = 0, ref_count = 0, total_gap_days = 0, total_work_days = 0
|
||||
, gap_start;
|
||||
|
||||
new_e.forEach( function(point) {
|
||||
var inc = point[0] === 'start' ? 1 : -1;
|
||||
ref_count += inc;
|
||||
if( ref_count === 0 ) {
|
||||
coverage.gaps.push( { start: point[1], end: null });
|
||||
}
|
||||
else if( ref_count === 1 && inc === 1 ) {
|
||||
var lastGap = _.last( coverage.gaps );
|
||||
if( lastGap ) {
|
||||
lastGap.end = point[1];
|
||||
lastGap.duration = lastGap.end.diff( lastGap.start, 'days' );
|
||||
total_gap_days += lastGap.duration;
|
||||
}
|
||||
}
|
||||
else if( ref_count === 2 && inc === 1 ) {
|
||||
coverage.overlaps.push( { start: point[1], end: null });
|
||||
}
|
||||
else if( ref_count === 1 && inc === -1 ) {
|
||||
var lastOver = _.last( coverage.overlaps );
|
||||
if( lastOver ) {
|
||||
lastOver.end = point[1];
|
||||
lastOver.duration = lastOver.end.diff( lastOver.start, 'days' );
|
||||
if( lastOver.duration === 0 ) {
|
||||
coverage.overlaps.pop();
|
||||
}
|
||||
total_work_days += lastOver.duration;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// It's possible that the last overlap didn't have an explicit .end date.
|
||||
// If so, set the end date to the present date and compute the overlap
|
||||
// duration normally.
|
||||
if( coverage.overlaps.length ) {
|
||||
if( !_.last( coverage.overlaps ).end ) {
|
||||
var l = _.last( coverage.overlaps );
|
||||
l.end = moment();
|
||||
l.duration = l.end.diff( l.start, 'days' );
|
||||
}
|
||||
}
|
||||
|
||||
var dur = {
|
||||
total: rez.duration('days'),
|
||||
work: total_work_days,
|
||||
gaps: total_gap_days
|
||||
};
|
||||
coverage.pct = ( dur.total > 0 && dur.work > 0 ) ?
|
||||
((((dur.total - dur.gaps) / dur.total) * 100)).toFixed(1) + '%' :
|
||||
'???';
|
||||
coverage.duration = dur;
|
||||
|
||||
return coverage;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
}());
|
71
src/inspectors/keyword-inspector.js
Normal file
71
src/inspectors/keyword-inspector.js
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
Keyword analysis for HackMyResume.
|
||||
@license MIT. See LICENSE.md for details.
|
||||
@module keyword-inspector.js
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
|
||||
|
||||
var _ = require('underscore');
|
||||
var FluentDate = require('../core/fluent-date');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Analyze the resume's use of keywords.
|
||||
@class keywordInspector
|
||||
*/
|
||||
var keywordInspector = module.exports = {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
A unique name for this inspector.
|
||||
*/
|
||||
moniker: 'keyword-inspector',
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Run the Keyword Inspector on a resume.
|
||||
@method run
|
||||
@return An collection of statistical keyword data.
|
||||
*/
|
||||
run: function( rez ) {
|
||||
|
||||
// http://stackoverflow.com/a/2593661/4942583
|
||||
function regex_quote(str) {
|
||||
return (str + '').replace(/[.?*+^$[\]\\(){}|-]/ig, "\\$&");
|
||||
}
|
||||
|
||||
var searchable = '';
|
||||
rez.transformStrings( ['imp', 'computed', 'safe'], function trxString( key, val ) {
|
||||
searchable += ' ' + val;
|
||||
});
|
||||
|
||||
return rez.keywords().map(function(kw) {
|
||||
//var regex = new RegExp( '\\b' + regex_quote( kw )/* + '\\b'*/, 'ig');
|
||||
var regex = new RegExp( regex_quote( kw ), 'ig');
|
||||
var myArray, count = 0;
|
||||
while ((myArray = regex.exec( searchable )) !== null) {
|
||||
count++;
|
||||
}
|
||||
return {
|
||||
name: kw,
|
||||
count: count
|
||||
};
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
}());
|
59
src/inspectors/totals-inspector.js
Normal file
59
src/inspectors/totals-inspector.js
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
Section analysis for HackMyResume.
|
||||
@license MIT. See LICENSE.md for details.
|
||||
@module totals-inspector.js
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
|
||||
|
||||
var _ = require('underscore');
|
||||
var FluentDate = require('../core/fluent-date');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Retrieve sectional overview and summary information.
|
||||
@class totalsInspector
|
||||
*/
|
||||
var totalsInspector = module.exports = {
|
||||
|
||||
|
||||
|
||||
moniker: 'totals-inspector',
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Run the Totals Inspector on a resume.
|
||||
@method run
|
||||
@return An array of objects containing summary information for each section
|
||||
on the resume.
|
||||
*/
|
||||
run: function( rez ) {
|
||||
|
||||
var ret = { };
|
||||
_.each( rez, function(val, key){
|
||||
if( _.isArray( val ) && !_.isString(val) ) {
|
||||
ret[ key ] = val.length;
|
||||
}
|
||||
else if( val.history && _.isArray( val.history ) ) {
|
||||
ret[ key ] = val.history.length;
|
||||
}
|
||||
else if( val.sets && _.isArray( val.sets ) ) {
|
||||
ret[ key ] = val.sets.length;
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
}());
|
16
src/use.txt
16
src/use.txt
@ -2,23 +2,25 @@ Usage:
|
||||
|
||||
hackmyresume <COMMAND> <SOURCES> [TO <TARGETS>] [-t <THEME>] [-f <FORMAT>]
|
||||
|
||||
<COMMAND> should be BUILD, NEW, 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. <FORMAT> should be either FRESH (for a FRESH-format resume) or JRS
|
||||
(for a JSON Resume-format resume).
|
||||
<COMMAND> should be BUILD, NEW, CONVERT, VALIDATE, ANALYZE 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.
|
||||
<FORMAT> should be either FRESH (for a FRESH-format resume) or JRS (for a JSON
|
||||
Resume-format resume).
|
||||
|
||||
hackmyresume BUILD resume.json TO out/resume.all
|
||||
hackmyresume NEW resume.json
|
||||
hackmyresume CONVERT resume.json TO resume-jrs.json
|
||||
hackmyresume ANALYZE resume.json
|
||||
hackmyresume VALIDATE resume.json
|
||||
|
||||
Both SOURCES and TARGETS can accept multiple files:
|
||||
|
||||
hackmyresume BUILD r1.json r2.json TO out/resume.all out2/resume.html
|
||||
hackmyresume NEW r1.json r2.json r3.json
|
||||
hackmyresume ANALYZE r1.json r2.json r3.json
|
||||
hackmyresume VALIDATE resume.json resume2.json resume3.json
|
||||
|
||||
See https://github.com/hacksalot/hackmyresume/blob/master/README.md for more
|
||||
|
12
src/utils/file-contains.js
Normal file
12
src/utils/file-contains.js
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
Definition of the SyntaxErrorEx class.
|
||||
@module file-contains.js
|
||||
*/
|
||||
|
||||
(function(){
|
||||
|
||||
module.exports = function( file, needle ) {
|
||||
return require('fs').readFileSync(file,'utf-8').indexOf( needle ) > -1;
|
||||
};
|
||||
|
||||
}());
|
24
src/utils/safe-json-loader.js
Normal file
24
src/utils/safe-json-loader.js
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
Definition of the SafeJsonLoader class.
|
||||
@module syntax-error-ex.js
|
||||
@license MIT. See LICENSE.md for details.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
var FS = require('fs');
|
||||
|
||||
module.exports = function loadSafeJson( file ) {
|
||||
try {
|
||||
return JSON.parse( FS.readFileSync( file ) );
|
||||
}
|
||||
catch(ex) {
|
||||
loadSafeJson.error = ex;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
}());
|
@ -1,6 +1,7 @@
|
||||
/**
|
||||
Definition of the SyntaxErrorEx class.
|
||||
@module syntax-error-ex.js
|
||||
@license MIT. See LICENSE.md for details.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
@ -26,7 +27,7 @@ Definition of the SyntaxErrorEx class.
|
||||
colNum = ex.columnNumber;
|
||||
}
|
||||
if( lineNum === null || colNum === null ) {
|
||||
var JSONLint = require('json-lint');
|
||||
var JSONLint = require('json-lint'); // TODO: json-lint or is-my-json-valid?
|
||||
var lint = JSONLint( rawData, { comments: false } );
|
||||
if( lineNum === null ) lineNum = (lint.error ? lint.line : '???');
|
||||
if( colNum === null ) colNum = (lint.error ? lint.character : '???');
|
||||
|
107
src/verbs/analyze.js
Normal file
107
src/verbs/analyze.js
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
Implementation of the 'analyze' verb for HackMyResume.
|
||||
@module create.js
|
||||
@license MIT. See LICENSE.md for details.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
|
||||
|
||||
|
||||
var MKDIRP = require('mkdirp')
|
||||
, PATH = require('path')
|
||||
, _ = require('underscore')
|
||||
, ResumeFactory = require('../core/resume-factory')
|
||||
, chalk = require('chalk');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Run the 'analyze' command.
|
||||
*/
|
||||
module.exports = function analyze( sources, dst, opts, logger ) {
|
||||
var _log = logger || console.log;
|
||||
if( !sources || !sources.length ) throw { fluenterror: 3 };
|
||||
|
||||
var nlzrs = _loadInspectors();
|
||||
|
||||
sources.forEach( function(src) {
|
||||
var result = ResumeFactory.loadOne( src, {
|
||||
log: _log, format: 'FRESH', objectify: true, throw: false
|
||||
});
|
||||
result.error || _analyze( result, nlzrs, opts, _log );
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Analyze a single resume.
|
||||
*/
|
||||
function _analyze( resumeObject, nlzrs, opts, log ) {
|
||||
var rez = resumeObject.rez;
|
||||
var safeFormat =
|
||||
(rez.meta && rez.meta.format && rez.meta.format.startsWith('FRESH')) ?
|
||||
'FRESH' : 'JRS';
|
||||
|
||||
var padding = 20;
|
||||
log(chalk.cyan('Analyzing ') + chalk.cyan.bold(safeFormat) +
|
||||
chalk.cyan(' resume: ') + chalk.cyan.bold(resumeObject.file));
|
||||
var info = _.mapObject( nlzrs, function(val, key) {
|
||||
return val.run( resumeObject.rez );
|
||||
});
|
||||
|
||||
log(chalk.cyan.bold('\nSECTIONS') + chalk.cyan(' (') + chalk.white.bold(_.keys(info.totals).length) + chalk.cyan('):\n'));
|
||||
var pad = require('string-padding');
|
||||
_.each( info.totals, function(tot, key) {
|
||||
log(chalk.cyan(pad(key + ': ',20)) + chalk.cyan.bold(pad(tot.toString(),5)));
|
||||
});
|
||||
|
||||
log();
|
||||
log(chalk.cyan.bold('COVERAGE') + chalk.cyan(' (') + chalk.white.bold( info.coverage.pct ) + chalk.cyan('):\n'));
|
||||
log(chalk.cyan(pad('Total Days: ', padding)) + chalk.cyan.bold( pad(info.coverage.duration.total.toString(),5) ));
|
||||
log(chalk.cyan(pad('Employed: ', padding)) + chalk.cyan.bold( pad((info.coverage.duration.total - info.coverage.duration.gaps).toString(),5) ));
|
||||
log(chalk.cyan(pad('Gaps: ', padding + 4)) + chalk.cyan.bold(info.coverage.gaps.length) + chalk.cyan(' [') + info.coverage.gaps.map(function(g) {
|
||||
var clr = 'green';
|
||||
if( g.duration > 35 ) clr = 'yellow';
|
||||
if( g.duration > 90 ) clr = 'red';
|
||||
return chalk[clr].bold( g.duration) ;
|
||||
}).join(', ') + chalk.cyan(']') );
|
||||
log(chalk.cyan(pad('Overlaps: ', padding + 4)) + chalk.cyan.bold(info.coverage.overlaps.length) + chalk.cyan(' [') + info.coverage.overlaps.map(function(ol) {
|
||||
var clr = 'green';
|
||||
if( ol.duration > 35 ) clr = 'yellow';
|
||||
if( ol.duration > 90 ) clr = 'red';
|
||||
return chalk[clr].bold( ol.duration) ;
|
||||
}).join(', ') + chalk.cyan(']') );
|
||||
|
||||
var tot = 0;
|
||||
log();
|
||||
log( chalk.cyan.bold('KEYWORDS') + chalk.cyan(' (') + chalk.white.bold( info.keywords.length ) +
|
||||
chalk.cyan('):\n\n') +
|
||||
info.keywords.map(function(g) {
|
||||
tot += g.count;
|
||||
return chalk.cyan( pad(g.name + ': ', padding) ) + chalk.cyan.bold( pad( g.count.toString(), 5 )) + chalk.cyan(' mentions');
|
||||
}).join('\n'));
|
||||
|
||||
log(chalk.cyan( pad('TOTAL: ', padding) ) + chalk.white.bold( pad( tot.toString(), 5 )) + chalk.cyan(' mentions'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Load inspectors.
|
||||
*/
|
||||
function _loadInspectors() {
|
||||
return {
|
||||
totals: require('../inspectors/totals-inspector'),
|
||||
coverage: require('../inspectors/gap-inspector'),
|
||||
keywords: require('../inspectors/keyword-inspector')
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
}());
|
@ -4,34 +4,66 @@ Implementation of the 'convert' verb for HackMyResume.
|
||||
@license MIT. See LICENSE.md for details.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
|
||||
var ResumeFactory = require('../core/resume-factory');
|
||||
|
||||
|
||||
var ResumeFactory = require('../core/resume-factory')
|
||||
, chalk = require('chalk')
|
||||
, HACKMYSTATUS = require('../core/status-codes');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Convert between FRESH and JRS formats.
|
||||
*/
|
||||
module.exports = function convert( sources, dst, opts, logger ) {
|
||||
module.exports = function convert( srcs, dst, opts, logger ) {
|
||||
|
||||
// Housekeeping
|
||||
var _log = logger || console.log;
|
||||
if( !sources || !sources.length ) { throw { fluenterror: 6 }; }
|
||||
if( !srcs || !srcs.length ) { throw { fluenterror: 6 }; }
|
||||
if( !dst || !dst.length ) {
|
||||
if( sources.length === 1 ) { throw { fluenterror: 5 }; }
|
||||
else if( sources.length === 2 ) { dst = [ sources[1] ]; sources = [ sources[0] ]; }
|
||||
else { throw { fluenterror: 5 }; }
|
||||
if( srcs.length === 1 ) {
|
||||
throw { fluenterror: HACKMYSTATUS.inputOutputParity };
|
||||
}
|
||||
else if( srcs.length === 2 ) {
|
||||
dst = dst || []; dst.push( srcs.pop() );
|
||||
}
|
||||
else {
|
||||
throw { fluenterror: HACKMYSTATUS.inputOutputParity };
|
||||
}
|
||||
}
|
||||
if( sources && dst && sources.length && dst.length && sources.length !== dst.length ) {
|
||||
throw { fluenterror: 7 };
|
||||
if(srcs && dst && srcs.length && dst.length && srcs.length !== dst.length){
|
||||
throw { fluenterror: HACKMYSTATUS.inputOutputParity };
|
||||
}
|
||||
var sourceResumes = ResumeFactory.load( sources, _log, null, true );
|
||||
sourceResumes.forEach(function( src, idx ) {
|
||||
var sheet = src.rez;
|
||||
var sourceFormat = ((sheet.basics && sheet.basics.imp) || sheet.imp).orgFormat === 'JRS' ? 'JRS' : 'FRESH';
|
||||
var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS';
|
||||
_log( 'Converting '.useful + src.file.useful.bold + (' (' +
|
||||
sourceFormat + ') to ').useful + dst[0].useful.bold +
|
||||
(' (' + targetFormat + ').').useful );
|
||||
sheet.saveAs( dst[idx], targetFormat );
|
||||
|
||||
// Load source resumes
|
||||
srcs.forEach( function( src, idx ) {
|
||||
|
||||
// Load the resume
|
||||
var rinfo = ResumeFactory.loadOne( src, {
|
||||
log: _log, format: null, objectify: true, throw: true
|
||||
});
|
||||
|
||||
var s = rinfo.rez
|
||||
, srcFmt = ((s.basics && s.basics.imp) || s.imp).orgFormat === 'JRS' ?
|
||||
'JRS' : 'FRESH'
|
||||
, targetFormat = srcFmt === 'JRS' ? 'FRESH' : 'JRS';
|
||||
|
||||
// TODO: Core should not log
|
||||
_log( chalk.green('Converting ') + chalk.green.bold(rinfo.file) +
|
||||
chalk.green(' (' + srcFmt + ') to ') + chalk.green.bold(dst[idx]) +
|
||||
chalk.green(' (' + targetFormat + ').'));
|
||||
|
||||
// Save it to the destination format
|
||||
s.saveAs( dst[idx], targetFormat );
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
}());
|
||||
|
@ -6,9 +6,9 @@ Implementation of the 'create' verb for HackMyResume.
|
||||
|
||||
(function(){
|
||||
|
||||
var FLUENT = require('../hackmyapi')
|
||||
, MKDIRP = require('mkdirp')
|
||||
, PATH = require('path');
|
||||
var MKDIRP = require('mkdirp')
|
||||
, PATH = require('path')
|
||||
, chalk = require('chalk');
|
||||
|
||||
/**
|
||||
Create a new empty resume in either FRESH or JRS format.
|
||||
@ -18,10 +18,12 @@ Implementation of the 'create' verb for HackMyResume.
|
||||
if( !src || !src.length ) throw { fluenterror: 8 };
|
||||
src.forEach( function( t ) {
|
||||
var safeFormat = opts.format.toUpperCase();
|
||||
_log('Creating new '.useful +safeFormat.useful.bold +
|
||||
' resume: '.useful + t.useful.bold);
|
||||
_log(chalk.green('Creating new ') + chalk.green.bold(safeFormat) +
|
||||
chalk.green(' resume: ') + chalk.green.bold(t));
|
||||
MKDIRP.sync( PATH.dirname( t ) ); // Ensure dest folder exists;
|
||||
FLUENT[ safeFormat + 'Resume' ].default().save( t );
|
||||
var RezClass = require('../core/' + safeFormat.toLowerCase() + '-resume' );
|
||||
RezClass.default().save(t);
|
||||
//FLUENT[ safeFormat + 'Resume' ].default().save( t );
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -3,6 +3,8 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
@module generate.js
|
||||
@license MIT. See LICENSE.md for details.
|
||||
*/
|
||||
// TODO: EventEmitter
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
@ -21,6 +23,8 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
, _ = require('underscore')
|
||||
, _fmts = require('../core/default-formats')
|
||||
, extend = require('../utils/extend')
|
||||
, chalk = require('chalk')
|
||||
, pad = require('string-padding')
|
||||
, _err, _log, rez;
|
||||
|
||||
|
||||
@ -44,32 +48,54 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
*/
|
||||
function build( src, dst, opts, logger, errHandler ) {
|
||||
|
||||
// Housekeeping...
|
||||
// Housekeeping
|
||||
//_opts = extend( true, _opts, opts );
|
||||
_log = logger || console.log;
|
||||
_err = errHandler || error;
|
||||
//_opts = extend( true, _opts, opts );
|
||||
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
|
||||
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
|
||||
_opts.css = opts.css;
|
||||
_opts.css = opts.css || 'embed';
|
||||
_opts.pdf = opts.pdf;
|
||||
_opts.wrap = opts.wrap || 60;
|
||||
_opts.stitles = opts.sectionTitles;
|
||||
_opts.tips = opts.tips;
|
||||
//_opts.noTips = opts.noTips;
|
||||
|
||||
// Load the theme...
|
||||
// If two or more files are passed to the GENERATE command and the TO
|
||||
// keyword is omitted, the last file specifies the output file.
|
||||
if( src.length > 1 && ( !dst || !dst.length ) ) {
|
||||
dst.push( src.pop() );
|
||||
}
|
||||
|
||||
// Load the theme...we do this first because the theme choice (FRESH or
|
||||
// JSON Resume) determines what format we'll convert the resume to.
|
||||
var tFolder = verify_theme( _opts.theme );
|
||||
var theme = load_theme( tFolder );
|
||||
|
||||
// Load input resumes...
|
||||
if( !src || !src.length ) { throw { fluenterror: 3 }; }
|
||||
var sheets = ResumeFactory.load(src, _log, theme.render ? 'JRS' : 'FRESH', true);
|
||||
var sheets = ResumeFactory.load(src, {
|
||||
log: _log, format: theme.render ? 'JRS' : 'FRESH',
|
||||
objectify: true, throw: true
|
||||
}).map(function(sh){ return sh.rez; });
|
||||
|
||||
// Merge input resumes...
|
||||
var msg = '';
|
||||
var rezRep = _.reduceRight( sheets, function( a, b, idx ) {
|
||||
rez = _.reduceRight( sheets, function( a, b, idx ) {
|
||||
msg += ((idx == sheets.length - 2) ?
|
||||
'Merging '.gray + a.rez.imp.fileName : '') + ' onto '.gray + b.rez.fileName;
|
||||
return extend( true, b.rez, a.rez );
|
||||
chalk.cyan('Merging ') + chalk.cyan.bold(a.i().file) : '') +
|
||||
chalk.cyan(' onto ') + chalk.cyan.bold(b.i().file);
|
||||
return extend( true, b, a );
|
||||
});
|
||||
rez = rezRep.rez;
|
||||
msg && _log(msg);
|
||||
|
||||
// Output theme messages
|
||||
var numFormats = Object.keys(theme.formats).length;
|
||||
var themeName = theme.name.toUpperCase();
|
||||
_log( chalk.yellow('Applying ') + chalk.yellow.bold(themeName) +
|
||||
chalk.yellow(' theme (' + numFormats + ' format' +
|
||||
( numFormats === 1 ? ')' : 's)') ));
|
||||
|
||||
// Expand output resumes...
|
||||
var targets = expand( dst, theme );
|
||||
|
||||
@ -78,6 +104,30 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
t.final = single( t, theme, targets );
|
||||
});
|
||||
|
||||
if( _opts.tips && (theme.message || theme.render) ) {
|
||||
var WRAP = require('word-wrap');
|
||||
if( theme.message ) {
|
||||
_log( WRAP( chalk.gray('The ' + themeName +
|
||||
' theme says: "') + chalk.white(theme.message) + chalk.gray('"'),
|
||||
{ width: _opts.wrap, indent: '' } ));
|
||||
}
|
||||
else {
|
||||
_log( WRAP( chalk.gray('The ' + themeName +
|
||||
' theme says: "') + chalk.white('For best results view JSON Resume ' +
|
||||
'themes over a local or remote HTTP connection. For example:'),
|
||||
{ width: _opts.wrap, indent: '' }
|
||||
));
|
||||
_log('');
|
||||
_log(
|
||||
' npm install http-server -g\r' +
|
||||
' http-server <resume-folder>' );
|
||||
_log('');
|
||||
_log(chalk.white('For more information, see the README."'),
|
||||
{ width: _opts.wrap, indent: '' } );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Don't send the client back empty-handed
|
||||
return { sheet: rez, targets: targets, processed: targets };
|
||||
}
|
||||
@ -102,9 +152,28 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
, fName = PATH.basename(f, '.' + fType)
|
||||
, theFormat;
|
||||
|
||||
_log( 'Generating '.useful +
|
||||
targInfo.fmt.outFormat.toUpperCase().useful.bold +
|
||||
' resume: '.useful + PATH.relative(process.cwd(), f ).useful.bold );
|
||||
var suffix = '';
|
||||
if( targInfo.fmt.outFormat === 'pdf' ) {
|
||||
if( _opts.pdf ) {
|
||||
if( _opts.pdf !== 'none' ) {
|
||||
suffix = chalk.green(' (with ' + _opts.pdf + ')');
|
||||
}
|
||||
else {
|
||||
_log( chalk.gray('Skipping ') +
|
||||
chalk.white.bold(
|
||||
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
|
||||
chalk.gray(' resume') + suffix + chalk.green(': ') +
|
||||
chalk.white( PATH.relative(process.cwd(), f )) );
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_log( chalk.green('Generating ') +
|
||||
chalk.green.bold(
|
||||
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
|
||||
chalk.green(' resume') + suffix + chalk.green(': ') +
|
||||
chalk.green.bold( PATH.relative(process.cwd(), f )) );
|
||||
|
||||
// If targInfo.fmt.files exists, this format is backed by a document.
|
||||
// Fluent/FRESH themes are handled here.
|
||||
@ -130,7 +199,7 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
// JSON Resume themes have a 'render' method that needs to be called
|
||||
if( theme.render ) {
|
||||
var COPY = require('copy');
|
||||
var globs = [ /*'**',*/ '*.css', '*.js', '*.png', '*.jpg', '*.gif', '*.bmp' ];
|
||||
var globs = [ '*.css', '*.js', '*.png', '*.jpg', '*.gif', '*.bmp' ];
|
||||
COPY.sync( globs , outFolder, {
|
||||
cwd: theme.folder, nodir: true,
|
||||
ignore: ['node_modules/','node_modules/**']
|
||||
@ -151,7 +220,7 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
console.log = consoleLog;
|
||||
|
||||
// Unharden
|
||||
rezHtml = rezHtml.replace( /@@@@~.+?~@@@@/g, function(val){
|
||||
rezHtml = rezHtml.replace( /@@@@~.*?~@@@@/gm, function(val){
|
||||
return MDIN( val.replace( /~@@@@/gm,'' ).replace( /@@@@~/gm,'' ) );
|
||||
});
|
||||
|
||||
@ -209,28 +278,22 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
|
||||
var to = PATH.resolve(t), pa = parsePath(to),fmat = pa.extname || '.all';
|
||||
|
||||
var explicitFormats = _.omit( theTheme.formats, function(val, key) {
|
||||
return !val.freebie;
|
||||
});
|
||||
var implicitFormats = _.omit( theTheme.formats, function(val) {
|
||||
return val.freebie;
|
||||
});
|
||||
|
||||
targets.push.apply(
|
||||
targets, fmat === '.all' ?
|
||||
Object.keys( implicitFormats ).map( function( k ) {
|
||||
Object.keys( theTheme.formats ).map( function( k ) {
|
||||
var z = theTheme.formats[k];
|
||||
return { file: to.replace( /all$/g, z.outFormat ), fmt: z };
|
||||
}) :
|
||||
[{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
|
||||
|
||||
targets.push.apply(
|
||||
targets, fmat === '.all' ?
|
||||
Object.keys( explicitFormats ).map( function( k ) {
|
||||
var z = theTheme.formats[k];
|
||||
return { file: to.replace( /all$/g, z.outFormat ), fmt: z };
|
||||
}) :
|
||||
[{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
|
||||
// targets.push.apply(
|
||||
// targets, fmat === '.all' ?
|
||||
// Object.keys( explicitFormats ).map( function( k ) {
|
||||
// var z = theTheme.formats[k];
|
||||
// return { file: to.replace( /all$/g, z.outFormat ), fmt: z };
|
||||
// }) :
|
||||
// [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
|
||||
|
||||
});
|
||||
|
||||
@ -242,9 +305,9 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
Verify the specified theme name/path.
|
||||
*/
|
||||
function verify_theme( themeNameOrPath ) {
|
||||
var tFolder = PATH.resolve(
|
||||
__dirname,
|
||||
'../../node_modules/fresh-themes/themes',
|
||||
var tFolder = PATH.join(
|
||||
parsePath ( require.resolve('fresh-themes') ).dirname,
|
||||
'/themes/',
|
||||
themeNameOrPath
|
||||
);
|
||||
var exists = require('path-exists').sync;
|
||||
@ -271,10 +334,6 @@ Implementation of the 'generate' verb for HackMyResume.
|
||||
// Cache the theme object
|
||||
_opts.themeObj = theTheme;
|
||||
|
||||
// Output a message TODO: core should not log
|
||||
var numFormats = Object.keys(theTheme.formats).length;
|
||||
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
|
||||
(' theme (' + numFormats + ' formats)').info);
|
||||
return theTheme;
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,8 @@ Implementation of the 'validate' verb for HackMyResume.
|
||||
var FS = require('fs');
|
||||
var ResumeFactory = require('../core/resume-factory');
|
||||
var SyntaxErrorEx = require('../utils/syntax-error-ex');
|
||||
var chalk = require('chalk');
|
||||
var HACKMYSTATUS = require('../core/status-codes');
|
||||
|
||||
module.exports =
|
||||
|
||||
@ -26,32 +28,43 @@ Implementation of the 'validate' verb for HackMyResume.
|
||||
jars: require('../core/resume.json')
|
||||
};
|
||||
|
||||
var resumes = ResumeFactory.load( sources, {
|
||||
log: _log,
|
||||
format: null,
|
||||
objectify: false,
|
||||
throw: false,
|
||||
muffle: true
|
||||
});
|
||||
|
||||
// Load input resumes...
|
||||
sources.forEach(function( src ) {
|
||||
resumes.forEach(function( src ) {
|
||||
|
||||
var result = ResumeFactory.loadOne( src, function(){}, null, false );
|
||||
if( result.error ) {
|
||||
_log( 'Validating '.info + src.infoBold + ' against '.info + 'AUTO'.infoBold + ' schema:'.info + ' BROKEN'.red.bold );
|
||||
if( src.error ) {
|
||||
// TODO: Core should not log
|
||||
_log( chalk.white('Validating ') + chalk.gray.bold(src.file) +
|
||||
chalk.white(' against ') + chalk.gray.bold('AUTO') +
|
||||
chalk.white(' schema:') + chalk.red.bold(' BROKEN') );
|
||||
|
||||
var ex = result.error; // alias
|
||||
var ex = src.error; // alias
|
||||
if ( ex instanceof SyntaxError) {
|
||||
var info = new SyntaxErrorEx( ex, result.raw );
|
||||
_log( ('--> '.warn.bold + src.toUpperCase() + ' contains invalid JSON on line ' +
|
||||
info.line + ' column ' + info.col + '.').warn +
|
||||
' Unable to validate.'.warn );
|
||||
_log( (' INTERNAL: ' + ex).warn );
|
||||
var info = new SyntaxErrorEx( ex, src.raw );
|
||||
_log( chalk.red.bold('--> ' + src.file.toUpperCase() + ' contains invalid JSON on line ' +
|
||||
info.line + ' column ' + info.col + '.' +
|
||||
chalk.red(' Unable to validate.') ) );
|
||||
_log( chalk.red.bold(' INTERNAL: ' + ex) );
|
||||
}
|
||||
else {
|
||||
_log(('ERROR: ' + ex.toString()).warn.bold);
|
||||
_log(chalk.red.bold('ERROR: ' + ex.toString()));
|
||||
}
|
||||
if( opts.assert ) throw { fluenterror: HACKMYSTATUS.invalid };
|
||||
return;
|
||||
}
|
||||
|
||||
var json = result.json;
|
||||
var json = src.json;
|
||||
var isValid = false;
|
||||
var style = 'useful';
|
||||
var style = 'green';
|
||||
var errors = [];
|
||||
var fmt = json.meta && (json.meta.format==='FRESH@0.1.0') ? 'fresh':'jars';
|
||||
var fmt = json.basics ? 'jrs' : 'fresh';
|
||||
|
||||
try {
|
||||
var validate = validator( schemas[ fmt ], { // Note [1]
|
||||
@ -62,7 +75,7 @@ Implementation of the 'validate' verb for HackMyResume.
|
||||
|
||||
isValid = validate( json );
|
||||
if( !isValid ) {
|
||||
style = 'warn';
|
||||
style = 'yellow';
|
||||
errors = validate.errors;
|
||||
}
|
||||
|
||||
@ -71,16 +84,20 @@ Implementation of the 'validate' verb for HackMyResume.
|
||||
return;
|
||||
}
|
||||
|
||||
_log( 'Validating '.info + result.file.infoBold + ' against '.info +
|
||||
fmt.replace('jars','JSON Resume').toUpperCase().infoBold +
|
||||
' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold );
|
||||
_log( chalk.white('Validating ') + chalk.white.bold(src.file) + chalk.white(' against ') +
|
||||
chalk.white.bold(fmt.replace('jars','JSON Resume').toUpperCase()) +
|
||||
chalk.white(' schema: ') + chalk[style].bold(isValid ? 'VALID!' : 'INVALID') );
|
||||
|
||||
errors.forEach(function(err,idx) {
|
||||
_log( '--> '.bold.yellow +
|
||||
(err.field.replace('data.','resume.').toUpperCase() + ' ' +
|
||||
err.message).yellow );
|
||||
_log( chalk.yellow.bold('--> ') +
|
||||
chalk.yellow(err.field.replace('data.','resume.').toUpperCase() + ' ' +
|
||||
err.message) );
|
||||
});
|
||||
|
||||
if( opts.assert && !isValid ) {
|
||||
throw { fluenterror: HACKMYSTATUS.invalid, shouldExit: true };
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -5,9 +5,8 @@ var chai = require('chai')
|
||||
, path = require('path')
|
||||
, _ = require('underscore')
|
||||
, FRESHResume = require('../src/core/fresh-resume')
|
||||
, FCMD = require( '../src/hackmycmd')
|
||||
, validator = require('is-my-json-valid')
|
||||
, COLORS = require('colors');
|
||||
, FCMD = require( '../src/hackmyapi')
|
||||
, validator = require('is-my-json-valid');
|
||||
|
||||
chai.config.includeStack = false;
|
||||
|
||||
@ -19,22 +18,11 @@ describe('Testing CLI interface', function () {
|
||||
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
var opts = {
|
||||
//theme: 'compact',
|
||||
format: 'FRESH',
|
||||
prettify: true,
|
||||
silent: true
|
||||
silent: false,
|
||||
assert: true // Causes validation errors to throw exceptions
|
||||
};
|
||||
|
||||
var opts2 = {
|
||||
@ -47,17 +35,29 @@ describe('Testing CLI interface', function () {
|
||||
run( 'new', ['test/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
|
||||
run( 'new', ['test/sandbox/new-1.json', 'test/sandbox/new-2.json', 'test/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' );
|
||||
run( 'new', ['test/sandbox/new-jrs-1.json', 'test/sandbox/new-jrs-2.json', 'test/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' );
|
||||
run( 'new', ['test/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
|
||||
fail( 'new', [], [], opts, " (when a filename isn't specified)" );
|
||||
|
||||
run( 'validate', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], [], opts, ' (FRESH format)' );
|
||||
run( 'validate', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
|
||||
run( 'validate', ['node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json'], [], opts, ' (jane-q-fullstacker|FRESH)' );
|
||||
run( 'validate', ['node_modules/fresh-test-resumes/src/johnny-trouble.fresh.json'], [], opts, ' (johnny-trouble|FRESH)' );
|
||||
fail( 'validate', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (new-fresh-resume|FRESH)' );
|
||||
run( 'validate', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks.json|JRS)' );
|
||||
run( 'validate', ['test/resumes/jrs-0.0.0/jane-incomplete.json'], [], opts2, ' (jane-incomplete.json|JRS)' );
|
||||
fail( 'validate', ['test/sandbox/new-1.json','test/sandbox/new-jrs-resume.json','test/sandbox/new-1.json', 'test/sandbox/new-2.json', 'test/sandbox/new-3.json'], [], opts, ' (5|BOTH)' );
|
||||
|
||||
run( 'analyze', ['node_modules/fresh-test-resumes/src/jane-fullstacker.json'], [], opts, ' (jane-q-fullstacker|FRESH)' );
|
||||
run( 'analyze', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks|JRS)' );
|
||||
|
||||
run( 'build',
|
||||
[ 'node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json',
|
||||
'node_modules/fresh-test-resumes/src/override/jane-fullstacker-override.fresh.json' ],
|
||||
[ 'test/sandbox/merged/jane-fullstacker-gamedev.fresh.all'], opts, ' (jane-q-fullstacker w/ override|FRESH)'
|
||||
);
|
||||
|
||||
function run( verb, src, dst, opts, msg ) {
|
||||
msg = msg || '.';
|
||||
it( 'The ' + verb.toUpperCase() + ' command should SUCCEED' + msg, function () {
|
||||
function runIt() {
|
||||
FCMD.verbs[verb]( src, dst, opts, opts.silent ? logMsg : null );
|
||||
FCMD.verbs[verb]( src, dst, opts, opts.silent ? logMsg : function(msg){ msg = msg || ''; console.log(msg); } );
|
||||
}
|
||||
runIt.should.not.Throw();
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ var chai = require('chai')
|
||||
, expect = chai.expect
|
||||
, should = chai.should()
|
||||
, path = require('path')
|
||||
, parsePath = require('parse-filepath')
|
||||
, _ = require('underscore')
|
||||
, FRESHResume = require('../src/core/fresh-resume')
|
||||
, CONVERTER = require('../src/core/convert')
|
||||
@ -22,7 +23,7 @@ describe('FRESH/JRS converter', function () {
|
||||
var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' );
|
||||
|
||||
_sheet = new FRESHResume().open( fileA );
|
||||
MKDIRP.sync( path.parse(fileB).dir );
|
||||
MKDIRP.sync( parsePath( fileB ).dirname );
|
||||
_sheet.saveAs( fileB, 'JRS' );
|
||||
|
||||
var rawA = FS.readFileSync( fileA, 'utf8' );
|
||||
|
@ -58,5 +58,5 @@ function testResume(opts) {
|
||||
}
|
||||
|
||||
var sects = [ 'info', 'employment', 'service', 'skills', 'education', 'writing', 'recognition', 'references' ];
|
||||
testResume({ title: 'jane-q-fullstacker', path: 'node_modules/jane-q-fullstacker/resume/jane-resume.json', duration: 7, sections: sects });
|
||||
testResume({ title: 'johnny-trouble-resume', path: 'node_modules/johnny-trouble-resume/src/johnny-trouble.fresh.json', duration: 3, sections: sects });
|
||||
testResume({ title: 'jane-q-fullstacker', path: 'node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json', duration: 7, sections: sects });
|
||||
testResume({ title: 'johnny-trouble-resume', path: 'node_modules/fresh-test-resumes/src/johnny-trouble.fresh.json', duration: 4, sections: sects });
|
||||
|
@ -6,11 +6,13 @@ var SPAWNWATCHER = require('../src/core/spawn-watch')
|
||||
, path = require('path')
|
||||
, _ = require('underscore')
|
||||
, FRESHResume = require('../src/core/fresh-resume')
|
||||
, FCMD = require( '../src/hackmycmd')
|
||||
, HMR = require( '../src/hackmyapi')
|
||||
, validator = require('is-my-json-valid')
|
||||
, COLORS = require('colors');
|
||||
, READFILES = require('recursive-readdir-sync')
|
||||
, fileContains = require('../src/utils/file-contains')
|
||||
, FS = require('fs');
|
||||
|
||||
chai.config.includeStack = false;
|
||||
chai.config.includeStack = true;
|
||||
|
||||
function genThemes( title, src, fmt ) {
|
||||
|
||||
@ -18,17 +20,6 @@ function genThemes( title, src, fmt ) {
|
||||
|
||||
var _sheet;
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
function genTheme( fmt, src, themeName, themeLoc, testTitle ) {
|
||||
themeLoc = themeLoc || themeName;
|
||||
testTitle = themeName.toUpperCase() + ' theme (' + fmt + ') should generate without throwing an exception';
|
||||
@ -40,9 +31,20 @@ function genThemes( title, src, fmt ) {
|
||||
theme: themeLoc,
|
||||
format: fmt,
|
||||
prettify: true,
|
||||
silent: true
|
||||
silent: false,
|
||||
css: 'embed'
|
||||
};
|
||||
FCMD.verbs.build( src, dst, opts, function() {} );
|
||||
try {
|
||||
HMR.verbs.build( src, dst, opts, function(msg) {
|
||||
msg = msg || '';
|
||||
console.log(msg);
|
||||
});
|
||||
}
|
||||
catch(ex) {
|
||||
console.log(ex);
|
||||
console.log(ex.stack);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
tryOpen.should.not.Throw();
|
||||
});
|
||||
@ -63,6 +65,23 @@ function genThemes( title, src, fmt ) {
|
||||
|
||||
}
|
||||
|
||||
genThemes( 'jane-q-fullstacker', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], 'FRESH' );
|
||||
genThemes( 'johnny-trouble', ['node_modules/johnny-trouble-resume/src/johnny-trouble.fresh.json'], 'FRESH' );
|
||||
function folderContains( needle, haystack ) {
|
||||
return _.some( READFILES( path.join(__dirname, haystack) ), function( absPath ) {
|
||||
if( FS.lstatSync( absPath ).isFile() ) {
|
||||
if( fileContains( absPath, needle ) ) {
|
||||
console.log('Found invalid metadata in ' + absPath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
genThemes( 'jane-q-fullstacker', ['node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json'], 'FRESH' );
|
||||
genThemes( 'johnny-trouble', ['node_modules/fresh-test-resumes/src/johnny-trouble.fresh.json'], 'FRESH' );
|
||||
genThemes( 'richard-hendriks', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], 'JRS' );
|
||||
|
||||
describe('Verifying generated theme files...', function() {
|
||||
it('Generated files should not contain ICE.', function() {
|
||||
expect( folderContains('@@@@', 'sandbox') ).to.be.false;
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user