1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2025-05-12 00:27:08 +01:00

Compare commits

...

87 Commits

Author SHA1 Message Date
37a053722d Update Travis URLs. 2016-01-06 11:36:40 -05:00
12fcf3b0cb Fix package.json glitch. 2016-01-06 11:28:09 -05:00
43ad9c1c71 Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-06 11:24:02 -05:00
4f9207a868 Fix: formatDate helper references the moment method, not the momentDate object 2016-01-06 11:23:39 -05:00
3d1f589bc1 formatDate helper now will only use moment if date is valid. If it's not, will use the user inputted value or a fallback parameter, if it is provided 2016-01-06 11:23:38 -05:00
ae436a3b84 Scrub. 2016-01-06 11:18:50 -05:00
202bb44c76 Update contributors.
@robertmain @jjanusch
2016-01-06 11:18:31 -05:00
041c609ff0 Merge pull request #85 from jjanusch/feat/present-formatter
formatDate template helper error handling and fallback
2016-01-06 11:00:17 -05:00
712b504168 Support global theme partials (interim). 2016-01-06 10:48:51 -05:00
bc9f0d468f Update tests w/ new validation behavior. 2016-01-06 00:44:46 -05:00
2d20077c08 Support --assert option for validate command.
Cause HMR to return an error code if validation fails and the --assert
option is present.
2016-01-06 00:44:34 -05:00
f61deda4e8 Fix format detection error in validate logic. 2016-01-06 00:21:18 -05:00
8203fa50ae Prep convert.js. 2016-01-06 00:20:30 -05:00
c5eab0fd9c Scrub. 2016-01-05 23:59:41 -05:00
40e71238ac Scrub. 2016-01-05 23:46:01 -05:00
9d75b207d1 Formalize empty-fresh.json dependency. 2016-01-05 23:28:49 -05:00
9b52c396d3 Fix missing method rename. 2016-01-05 22:32:46 -05:00
2759727984 Add convenience method. 2016-01-05 22:26:16 -05:00
e230d640cb Rename imp() to i() (interim). 2016-01-05 22:02:11 -05:00
d69688697c Update README. 2016-01-05 19:48:11 -05:00
9f7ec62b18 Bump fresh-themes to 0.11.0-beta. 2016-01-05 10:26:29 -05:00
b1a02918ff Support --no-tips flag. 2016-01-05 10:10:24 -05:00
ec05f6737a Emit JSON Resume theme instructions. 2016-01-05 10:10:12 -05:00
da5db6477b Introduce --color and --no-color options.
These are handled by Chalk, but need to be registered with Commander.js
in order for Chalk to see them.
2016-01-05 09:42:39 -05:00
0f580efb2b Mention ANALYZE command in man page. 2016-01-05 09:38:42 -05:00
ff23ee508b Restore app title. 2016-01-05 09:38:21 -05:00
2819faeb6f Improve theme/format inheritance (interim). 2016-01-05 09:28:40 -05:00
d205e882f6 Introduce FRESH theme/format inheritance.
Support "inherits" property in theme.json (FRESH themes only).
2016-01-05 06:34:56 -05:00
3f40441b0a Bump FRESCA version to 0.3.0. 2016-01-05 05:09:29 -05:00
6185f20ec9 Sort project history by default. 2016-01-05 05:00:04 -05:00
6a61989eb4 Introduce {{dateRange}} and {{camelCase}} helpers. 2016-01-05 04:59:51 -05:00
d658a069cd Rename {{hasSection}} helper to {{section}}. 2016-01-05 04:59:26 -05:00
25688dbe8e Bump fresh-test-resume to 0.2.1. 2016-01-05 01:41:04 -05:00
98362b9687 Bump fresh-test-resumes version. 2016-01-05 00:04:10 -05:00
4c31c96891 Introduce has/hasSection helpers. 2016-01-05 00:03:54 -05:00
219209c6ca Fix logic glitch in {{sectionTitle}} helper. 2016-01-04 19:46:45 -05:00
eff9fc51cb Integrate fresh-test-resumes module. 2016-01-04 19:45:49 -05:00
2ba23ee80d Add support for user-definable section titles.
Introduce a {{sectionTitle}} helper; requires theme updates.
2016-01-04 16:20:48 -05:00
0f83f8f5c2 Merge remote-tracking branch 'refs/remotes/origin/master' into dev 2016-01-04 16:13:37 -05:00
4ba3a3f2a9 Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-04 08:09:12 -05:00
db486a48aa Update README. 2016-01-04 08:06:51 -05:00
2cab1195e8 Fix 'create' alias. 2016-01-04 07:25:48 -05:00
ce75f09210 Refactor API interface. 2016-01-04 07:23:20 -05:00
a8fed1b69b Add missing semicolon. 2016-01-04 04:15:13 -05:00
62ca2020d8 Bump FRESH themes version. 2016-01-04 04:15:01 -05:00
f65cf8880e Add support for external options file. 2016-01-04 02:50:00 -05:00
c8d4a3deb3 Handle global options.
Fix broken --silent flag and set up -o/-opts.
2016-01-04 01:49:35 -05:00
d5e2a45034 Output theme message on generate. 2016-01-04 00:58:41 -05:00
2465f2ce1c Fix gap analysis glitches. 2016-01-04 00:14:43 -05:00
f2001bcbb1 Add a couple baseline "analyze" tests. 2016-01-03 23:18:35 -05:00
d5afb3eb2e Handle missing dates during gap inspection. 2016-01-03 23:17:36 -05:00
c711cb7922 Improve sorting. 2016-01-03 23:17:18 -05:00
e45e0316f6 Remove extraneous regex. 2016-01-03 10:07:58 -05:00
08ab512f4c Add overlap analysis. 2016-01-03 09:48:43 -05:00
f2bf09bf96 Allow variable-unit resume duration. 2016-01-03 09:48:22 -05:00
75e2b1c131 Improve keyword acquisition. 2016-01-03 09:48:02 -05:00
0b7ef16a41 Improve accuracy of keyword counts. 2016-01-03 07:36:05 -05:00
247eec396c Fix string iteration filtering glitch. 2016-01-03 07:35:47 -05:00
46c7fa9838 Add baseline keyword analysis. 2016-01-03 06:39:46 -05:00
b3fb2c7130 Scrub. 2016-01-03 05:06:54 -05:00
c3ec3f28bd Introduce section totals inspector. 2016-01-03 05:03:31 -05:00
0a8ee721e8 Allow for multiple PDF engines / support Phantom PDFs.
Start formalizing PDF generation apparatus and support a `--pdf`
parameter allowing the user to specify the flavor of PDF generation.
2016-01-03 04:11:42 -05:00
8d7cf32988 Finish Commander.js integration. 2016-01-03 03:18:56 -05:00
655ecebaa5 Clean up comments. 2016-01-03 02:40:04 -05:00
8fc0fa99d3 Remove unnecessary indirection. 2016-01-03 02:39:43 -05:00
69e8adc1cc Remove 'minimist' dependency. 2016-01-03 02:25:39 -05:00
6b3396e01b Use Commander.js for invocations. 2016-01-03 02:22:26 -05:00
a95b52acd0 Refactor command processing. 2016-01-02 00:15:46 -05:00
47553b6def Fix ICE encoding issues.
Fix issue where @@@@ is appearing in generated resumes.
2016-01-01 20:27:46 -05:00
e4a549ed30 Tests: Add ICE detection test.
ICE is the internal boilerplate we use to freeze/unfreeze themes when
trying to force-feed them Markdown or other formatted data.
2016-01-01 20:26:47 -05:00
dd2148bb92 Fix: formatDate helper references the moment method, not the momentDate object 2016-01-01 18:05:33 -05:00
d8b9d86896 Scrub. 2016-01-01 17:30:57 -05:00
889bd4bfc5 formatDate helper now will only use moment if date is valid. If it's not, will use the user inputted value or a fallback parameter, if it is provided 2016-01-01 17:27:49 -05:00
13fc903b2b Catch JSON syntax errors for all commands.
...and emit line/column info.
2016-01-01 17:20:42 -05:00
8c8dbfed72 Adjust test paths. 2016-01-01 15:06:36 -05:00
2b669cf35c Tweak error handling for cmd params. 2016-01-01 15:06:16 -05:00
5a2d892b85 Scrub error-handler.js. 2016-01-01 14:59:21 -05:00
37a7c318d5 Remove stack trace for ENOENT. 2016-01-01 14:58:56 -05:00
43873efcab Tweak analyze command error. 2016-01-01 14:38:52 -05:00
bb28e5aa8e Support --help option.
Support standard syntax for the HELP command.
2016-01-01 14:38:00 -05:00
c17261cd25 Merge pull request #81 from tjlav5/master
Fix relative theme directory
2016-01-01 13:46:55 -05:00
49e56cc226 Fix relative theme directory
The theme directory assumes it was a child of the HackMyResume module, but NPM3 will actually flatten this out. Following the same logic that the template-generator uses, find the path to the themes using NPMs require method.
2016-01-01 11:27:05 -05:00
84ad6cf356 Add missing chalk references. 2016-01-01 04:57:50 -05:00
b96526da31 Replace chalk with colors in tests. 2016-01-01 04:48:20 -05:00
cb14452df3 Replace colors with chalk.
Chalk has a few more options and doesn't mess around with
String.prototype.
2016-01-01 04:44:14 -05:00
d54b9a6d6c Remove unused method. 2016-01-01 03:45:14 -05:00
6285c2db3b Introduce "analyze" verb and framework.
Introduce a new "analyze" command and start setting up the inspector /
analyzer pipeline with a simple "gap analysis" inspector using a
reference-counted gap detection approach.
2016-01-01 03:39:48 -05:00
33 changed files with 1730 additions and 865 deletions

View File

@ -27,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.
@ -34,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
@ -80,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
@ -101,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>
@ -172,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
@ -188,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
@ -329,8 +344,8 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
[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/master
[travis-url-dev]: https://travis-ci.org/hacksalot/HackMyResume/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

View File

@ -1,6 +1,6 @@
{
"name": "hackmyresume",
"version": "1.3.1",
"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",

View File

@ -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){

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,8 @@ Status codes for HackMyResume.
inputOutputParity: 7,
createNameMissing: 8,
wkhtmltopdf: 9,
missingPackageJSON: 10
missingPackageJSON: 10,
invalid: 11
};
}());

View File

@ -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;
},
/**
@ -60,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
@ -76,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 ) {
@ -121,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;
},
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}
};
}());

View 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
};
});
}
};
}());

View 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;
}
};
}());

View File

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

View 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;
};
}());

View 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;
};
}());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {
@ -49,18 +37,27 @@ describe('Testing CLI interface', function () {
run( 'new', ['test/sandbox/new-jrs-1.json', 'test/sandbox/new-jrs-2.json', 'test/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' );
fail( 'new', [], [], opts, " (when a filename isn't specified)" );
run( 'validate', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], [], opts, ' (jane-q-fullstacker|FRESH)' );
run( 'validate', ['node_modules/johnny-trouble-resume/src/johnny-trouble.fresh.json'], [], opts, ' (johnny-trouble|FRESH)' );
run( 'validate', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (new-fresh-resume|FRESH)' );
run( 'validate', ['test/sandbox/resumes/jrs-0.0.0/ruchard-hendriks.json'], [], opts2, ' (richard-hendriks.json|JRS)' );
run( 'validate', ['test/sandbox/resumes/jrs-0.0.0/jane-incomplete.json'], [], opts2, ' (jane-incomplete.json|JRS)' );
run( '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( '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();
});

View File

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

View File

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