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

Compare commits

...

74 Commits

Author SHA1 Message Date
5de796b119 Merge pull request #35 from hacksalot/rel/v1.2.1
rel/v1.2.1
2015-12-24 07:41:12 -05:00
bf84341acf Version -> 1.2.1
Holding off on 1.3.0 pending HTTP/HTTPS support. #rebase #semver
2015-12-24 07:42:07 -05:00
bbac1fdceb Improve test coverage around incomplete JRS resumes.
Add quick sanity checks around incomplete or irregular JRS-format
resumes. Also decorate existing JRS test resumes (/tests/resumes/) with
current 0.0.0 JRS version ahead of 1.0.0 release.
2015-12-24 06:08:45 -05:00
c5ee1ee33c Quick fix for ".history" errors.
Affects #31 and #33.
2015-12-24 04:05:56 -05:00
c74eda90ed Introduce lodash dependency. 2015-12-24 04:05:17 -05:00
ef2fe95bd8 Remove unused method. 2015-12-24 04:04:44 -05:00
e2589b3730 Fix validate command error.
Still hitting some inconsistent behavior in different NPM
versions/platforms with invalid uppercase dependency names per
https://github.com/npm/npm/issues/3692. Partial fix for #33.
2015-12-24 03:23:56 -05:00
ebad1677bc Replace file-exists.js with NPM path-exists. 2015-12-22 18:55:17 -05:00
dab6ebfd82 Bump fluent-themes version to 0.8.0-beta. 2015-12-22 18:49:29 -05:00
dd61b5360a Update package.json contact info. 2015-12-22 18:49:11 -05:00
fced92a5a0 Bump version to 1.3.0. 2015-12-22 18:47:23 -05:00
64db1a654e Merge pull request #30 from hacksalot/rel/v1.2.0
rel/v1.2.0
2015-12-21 02:57:59 -05:00
31830ee759 Silence tests. 2015-12-21 03:04:40 -05:00
1c05846a4f Add CLI tests. 2015-12-21 02:58:16 -05:00
1db9c2e420 Fix README glitch. 2015-12-21 02:58:01 -05:00
c966f6766c Refactor verbs to separate files. 2015-12-21 02:56:02 -05:00
65b6359fd8 Bump version to 1.2.0. 2015-12-21 00:52:23 -05:00
a54476eede Reaffirm string-based generation.
In recent commits, HackMyResume generation logic, much like the pilots
in Top Gun who became too reliant on air-to-air missiles and lost the
true art of dogfighting, has become dependent on file-based generation
as implicit file assumptions have crept in. This commit reaffirms the
file-less, string-based nature of the generation process and, as a side
effect, adjusts the behavior of (binary) PDF generation to match.
2015-12-21 00:36:08 -05:00
7c0354046c Merge pull request #29 from hacksalot/final/v1.1.0
final/v1.1.0
2015-12-20 20:46:19 -05:00
43cd1c7e52 Allow TO keyword to be omitted.
If the TO keyword is missing, assume the last file passed in is the
destination file.
2015-12-20 20:53:21 -05:00
f80c333361 Scrub. 2015-12-20 20:22:46 -05:00
cdbb264093 Add string convenience method. 2015-12-20 18:42:02 -05:00
87b3bbe785 Bump version to 1.1.0. 2015-12-20 18:41:51 -05:00
b92cf7298a Refactor helpers.
Helpers shouldn't be specific to a given template engine (eg,
Handlebars) in order to allow sharing of helpers between different
template engines. Isolate abstract helpers in another module and apply
them via Handlebars.registerHelper and as necessary for other template
engines.
2015-12-20 18:24:28 -05:00
93456b5f40 Merge pull request #28 from hacksalot/rel-next
rel-next
2015-12-20 15:56:43 -05:00
72f29bf402 Update README. 2015-12-20 15:59:30 -05:00
f6fc384466 Add keywords to package.json. 2015-12-20 15:43:31 -05:00
c5ab3fdfae Bump version to 1.0.1. 2015-12-20 15:43:20 -05:00
78c5081a29 Support Markdown-driven hyperlinks in MS Word. 2015-12-20 15:42:31 -05:00
d0c181ee8c Merge pull request #26 from hacksalot/rel-v1.0.0
v1.0.0
2015-12-19 12:33:19 -05:00
80c6bb6e8b Rename to HMR. 2015-12-19 12:37:42 -05:00
786b3fd3b2 Merge pull request #25 from fluentdesk/v0.11.0
v0.11.0
2015-12-19 08:58:59 -05:00
f0a22be731 Skip underscore-prefixed folders during theme load. 2015-12-19 08:39:36 -05:00
ade60022fd Introduce new helpers. 2015-12-19 00:24:21 -05:00
7daba910ed Bump fluent-themes version. 2015-12-18 17:27:40 -05:00
a016d6d91a Update README. 2015-12-18 17:23:40 -05:00
fcaa97ed35 Change author info. 2015-12-18 15:35:00 -05:00
bb7373a229 Remove duplicate is-my-json-valid dependency. 2015-12-18 15:34:52 -05:00
759dcc30e7 Update tests. 2015-12-18 15:34:30 -05:00
0e47f02a33 Rename tests/jane-doe.json. 2015-12-18 15:34:00 -05:00
5fe90517e7 Auto-clean tests folder. 2015-12-18 15:33:18 -05:00
92128da381 Install URL-trimming helper. 2015-12-18 14:51:51 -05:00
1441fe3ae5 Class-ify Underscore/Handlebars engine. 2015-12-18 13:17:07 -05:00
b0bc71cd66 Introduce "either" helper for Handlebars themes. 2015-12-18 13:00:47 -05:00
e908e8bb34 Add missing Underscore require(). 2015-12-18 10:13:50 -05:00
d708a6c6d8 Refactor Handlebars helpers. 2015-12-18 10:10:30 -05:00
a630741098 Fix exception. 2015-12-18 10:10:19 -05:00
01d148e47c Bump version to 0.11.0. 2015-12-18 10:08:52 -05:00
dbd41ec439 Bump FRESCA version. 2015-12-17 14:28:24 -05:00
fc9cbab974 Choose template engine from active theme. 2015-12-17 11:04:29 -05:00
36f8010ebc Merge pull request #24 from fluentdesk/feat-notices
feat-notices
2015-12-17 10:27:58 -05:00
e80d8fb5c8 Bump version. 2015-12-17 10:29:22 -05:00
eabab26eef Update file headers. 2015-12-17 10:15:59 -05:00
18dbb23168 Merge pull request #23 from fluentdesk/v0.10.2
v0.10.2
2015-12-17 07:52:21 -05:00
9ad2a1e92e Add simple-html-tokenizer dependency. 2015-12-16 23:28:57 -05:00
5475b081b1 Support basic Markdown in MS Word docs. 2015-12-16 23:26:53 -05:00
ae9c295ce1 Better Handlebars support. 2015-12-16 20:13:27 -05:00
e0ef774692 Add missing semicolon. 2015-12-16 12:13:50 -05:00
0c1364593a Support post-save callback. 2015-12-16 11:26:30 -05:00
1603a4bc73 Prosecute FCVD updates. 2015-12-16 11:25:50 -05:00
8361cf9960 Remove extraneous comma.
Go away, extraneous comma.
2015-12-16 11:25:04 -05:00
8273e7d150 ... 2015-12-15 06:20:06 -05:00
5c49a8297f Fix: FCVD error. 2015-12-14 07:32:41 -05:00
ee1e4bf699 Scrub. 2015-12-12 11:13:47 -05:00
f5a8e36e50 Refactor theme tests. 2015-12-12 11:13:37 -05:00
b38a7c1da2 Improve conversions and tests. 2015-12-12 10:48:26 -05:00
fe2247329e Bump version to 0.10.2. 2015-12-12 04:43:29 -05:00
9d459370ce Update FRESH<-->JRS converter. 2015-12-12 04:42:56 -05:00
201d96fe22 Merge pull request #22 from fluentdesk/v0.10.1
v0.10.1
2015-12-11 22:29:58 -05:00
8747429bc6 Update NPM registry description. 2015-12-11 22:35:26 -05:00
95540efe29 Tweak FRESCA and theme dependency versions. 2015-12-11 04:02:05 -05:00
0474dc7dbe Bump version to 0.10.1. 2015-12-11 03:04:34 -05:00
e5af6c38e0 Merge remote-tracking branch 'refs/remotes/origin/master' into v0.10.1 2015-12-11 03:01:31 -05:00
00e6407347 Kludge theme loading issue for FCVD.
Resolve in v0.11.0.
2015-12-10 10:28:19 -05:00
54 changed files with 1934 additions and 918 deletions

View File

@ -17,6 +17,8 @@ module.exports = function (grunt) {
all: { src: ['tests/*.js'] }
},
clean: ['tests/sandbox'],
yuidoc: {
compile: {
name: '<%= pkg.name %>',
@ -46,10 +48,11 @@ module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-simple-mocha');
grunt.loadNpmTasks('grunt-contrib-yuidoc');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.registerTask('test', 'Test the FluentCV library.',
function( config ) { grunt.task.run( ['simplemocha:all'] ); });
grunt.registerTask('document', 'Generate FluentCV library documentation.',
grunt.registerTask('test', 'Test the HackMyResume library.',
function( config ) { grunt.task.run( ['clean','simplemocha:all'] ); });
grunt.registerTask('document', 'Generate HackMyResume library documentation.',
function( config ) { grunt.task.run( ['yuidoc'] ); });
grunt.registerTask('default', [ 'jshint', 'test', 'yuidoc' ]);

170
README.md
View File

@ -1,12 +1,14 @@
fluentCV
========
*Create polished technical résumés and CVs in multiple formats from your command
line or shell. See [FluentCV Desktop][7] for the desktop version. OS X ~ Windows
~ Linux.*
HackMyResume
============
*Create polished résumés and CVs in multiple formats from your command line or
shell. Author in clean Markdown and JSON, export to Word, HTML, PDF, LaTeX,
plain text, and other arbitrary formats. Fight the power, save trees. Compatible
with [FRESH][fresca] and [JRS][6] resumes.*
![](assets/fluentcv_cli_ubuntu.png)
![](assets/resume-bouqet.png)
FluentCV is a dev-friendly Swiss Army knife for resumes and CVs. Use it to:
HackMyResume is a dev-friendly, local-only Swiss Army knife for resumes and CVs.
Use it to:
1. **Generate** HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML,
YAML, print, smoke signal, carrier pigeon, and other arbitrary-format resumes
@ -14,7 +16,8 @@ and CVs, from a single source of truth&mdash;without violating DRY.
2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats.
3. **Validate** resumes against either format.
FluentCV supports both the [FRESH][fresca] and [JSON Resume][6] source formats.
HackMyResume is built with Node.js and runs on recent versions of OS X, Linux,
or Windows.
## Features
@ -22,8 +25,6 @@ FluentCV supports both the [FRESH][fresca] and [JSON Resume][6] source formats.
- 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.
- Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and
[FCV Desktop][7].
- Validate resumes against the FRESH or JSON Resume schema.
- Support for multiple input and output resumes.
- Use from your command line or [desktop][7].
@ -31,10 +32,10 @@ FluentCV supports both the [FRESH][fresca] and [JSON Resume][6] source formats.
## Install
Install FluentCV with NPM:
Install HackMyResume with NPM:
```bash
[sudo] npm install fluentcv -g
[sudo] npm install hackmyresume -g
```
Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for
@ -43,49 +44,52 @@ access to `xelatex` and similar.
## Getting Started
To use FluentCV you'll need to create a valid resume in either [FRESH][fresca]
or [JSON Resume][6] format. Then you can start using the command line tool.
There are four basic commands you should be aware of:
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:
- `build` generates resumes in HTML, Word, Markdown, PDF, and other formats. Use
it when you need to submit, upload, print, or email resumes in specific formats.
- **build** generates resumes in HTML, Word, Markdown, PDF, and other formats.
Use it when you need to submit, upload, print, or email resumes in specific
formats.
```bash
# fluentcv BUILD <INPUTS> TO <OUTPUTS> [-t THEME]
fluentcv BUILD resume.json TO out/resume.all
fluentcv BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all
# hackmyresume BUILD <INPUTS> TO <OUTPUTS> [-t THEME]
hackmyresume BUILD resume.json TO out/resume.all
hackmyresume BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all
```
- `new` creates a new resume in FRESH or JSON Resume format.
- **new** creates a new resume in FRESH or JSON Resume format.
```bash
# fluentcv NEW <OUTPUTS> [-f <FORMAT>]
fluentcv NEW resume.json
fluentcv NEW resume.json -f fresh
fluentcv NEW r1.json r2.json -f jrs
# hackmyresume NEW <OUTPUTS> [-f <FORMAT>]
hackmyresume NEW resume.json
hackmyresume NEW resume.json -f fresh
hackmyresume NEW r1.json r2.json -f jrs
```
- `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.
- **convert** converts your source resume between FRESH and JSON Resume
formats.
Use it to convert between the two formats to take advantage of tools and
services.
```bash
# fluentcv CONVERT <INPUTS> TO <OUTPUTS>
fluentcv CONVERT resume.json TO resume-jrs.json
fluentcv CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json
# hackmyresume CONVERT <INPUTS> TO <OUTPUTS>
hackmyresume CONVERT resume.json TO resume-jrs.json
hackmyresume CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json
```
- `validate` validates the specified resume against either the FRESH or JSON
- **validate** validates the specified resume against either the FRESH or JSON
Resume schema. Use it to make sure your resume data is sufficient and complete.
```bash
# fluentcv VALIDATE <INPUTS>
fluentcv VALIDATE resume.json
fluentcv VALIDATE r1.json r2.json r3.json
# hackmyresume VALIDATE <INPUTS>
hackmyresume VALIDATE resume.json
hackmyresume VALIDATE r1.json r2.json r3.json
```
## Supported Output Formats
FluentCV supports these output formats:
HackMyResume supports these output formats:
Output Format | Ext | Notes
------------- | --- | -----
@ -103,11 +107,11 @@ image | .png, .bmp | Forthcoming.
## Install
FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then:
HackMyResume requires a recent version of [Node.js][4] and [NPM][5]. Then:
1. Install the latest official [wkhtmltopdf][3] binary for your platform.
2. Optionally install an updated LaTeX environment (LaTeX resumes only).
2. Install **fluentCV** with `[sudo] npm install fluentcv -g`.
2. Install **HackMyResume** with `[sudo] npm install hackmyresume -g`.
3. You're ready to go.
## Use
@ -116,7 +120,7 @@ Assuming you've got a JSON-formatted resume handy, generating resumes in
different formats and combinations easy. Just run:
```bash
fluentcv BUILD <INPUTS> <OUTPUTS> [-t theme].
hackmyresume BUILD <INPUTS> <OUTPUTS> [-t theme].
```
Where `<INPUTS>` is one or more .json resume files, separated by spaces;
@ -125,25 +129,25 @@ theme (default to Modern). For example:
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.)
fluentcv build resume.json -o out/resume.all -t modern
hackmyresume build resume.json -o out/resume.all -t modern
# Generate a specific resume format
fluentcv build resume.json TO out/resume.html
fluentcv build resume.json TO out/resume.pdf
fluentcv build resume.json TO out/resume.md
fluentcv build resume.json TO out/resume.doc
fluentcv build resume.json TO out/resume.json
fluentcv build resume.json TO out/resume.txt
fluentcv build resume.json TO out/resume.yml
hackmyresume build resume.json TO out/resume.html
hackmyresume build resume.json TO out/resume.pdf
hackmyresume build resume.json TO out/resume.md
hackmyresume build resume.json TO out/resume.doc
hackmyresume build resume.json TO out/resume.json
hackmyresume build resume.json TO out/resume.txt
hackmyresume build resume.json TO out/resume.yml
# Specify 2 inputs and 3 outputs
fluentcv build in1.json in2.json TO out.html out.doc out.pdf
hackmyresume build in1.json in2.json TO out.html out.doc out.pdf
```
You should see something to the effect of:
```
*** FluentCV v0.9.0 ***
*** HackMyResume v0.9.0 ***
Reading JSON resume: foo/resume.json
Applying MODERN Theme (7 formats)
Generating HTML resume: out/resume.html
@ -159,28 +163,37 @@ 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.
You can specify a predefined or custom theme via the optional `-t` parameter.
For a predefined theme, include the theme name. For a custom theme, include the
path to the custom theme's folder.
```bash
fluentcv build resume.json -t modern
fluentcv build resume.json -t ~/foo/bar/my-custom-theme/
hackmyresume build resume.json -t modern
hackmyresume build resume.json -t ~/foo/bar/my-custom-theme/
```
As of v0.9.0, available predefined themes are `modern`, `minimist`, and `hello-world`, and `compact`.
As of v1.0.0, available predefined themes are `positive`, `modern`, `compact`,
`minimist`, and `hello-world`.
### Merging resumes
You can **merge multiple resumes together** by specifying them in order from most generic to most specific:
You can **merge multiple resumes together** by specifying them in order from
most generic to most specific:
```bash
# Merge specific.json onto base.json and generate all formats
fluentcv build base.json specific.json -o resume.all
hackmyresume build base.json specific.json -o resume.all
```
This can be useful for overriding a base (generic) resume with information from a specific (targeted) resume. For example, you might override your generic catch-all "software developer" resume with specific details from your targeted "game developer" resume, or combine two partial resumes into a "complete" resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge:
This can be useful for overriding a base (generic) resume with information from
a specific (targeted) resume. For example, you might override your generic
catch-all "software developer" resume with specific details from your targeted
"game developer" resume, or combine two partial resumes into a "complete"
resume. Merging follows conventional [extend()][9]-style behavior and there's
no arbitrary limit to how many resumes you can merge:
```bash
fluentcv build in1.json in2.json in3.json in4.json TO out.html out.doc
hackmyresume build in1.json in2.json in3.json in4.json TO out.html out.doc
Reading JSON resume: in1.json
Reading JSON resume: in2.json
Reading JSON resume: in3.json
@ -192,72 +205,75 @@ Generating WORD resume: out.doc
### Multiple targets
You can specify **multiple output targets** and FluentCV will build them:
You can specify **multiple output targets** and HackMyResume will build them:
```bash
# Generate out1.doc, out1.pdf, and foo.txt from me.json.
fluentcv build me.json -o out1.doc -o out1.pdf -o foo.txt
hackmyresume build me.json -o out1.doc -o out1.pdf -o foo.txt
```
You can also omit the output file(s) and/or theme completely:
```bash
# Equivalent to "fluentcv resume.json resume.all -t modern"
fluentcv build resume.json
# Equivalent to "hackmyresume resume.json resume.all -t modern"
hackmyresume build resume.json
```
### Using .all
The special `.all` extension tells FluentCV to generate all supported output formats for the given resume. For example, this...
The special `.all` extension tells HackMyResume to generate all supported output
formats for the given resume. For example, this...
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, etc.)
fluentcv build me.json -o out/resume.all
hackmyresume build me.json -o out/resume.all
```
..tells FluentCV to read `me.json` and generate `out/resume.md`, `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`.
..tells HackMyResume to read `me.json` and generate `out/resume.md`,
`out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and
`out/resume.json`.
### Validating
FluentCV can also validate your resumes against either the [FRESH /
HackMyResume can also validate your resumes against either the [FRESH /
FRESCA][fresca] or [JSON Resume][6] formats. To validate one or more existing
resumes, use the `validate` command:
```bash
# Validate myresume.json against either the FRESH or JSON Resume schema.
fluentcv validate resumeA.json resumeB.json
hackmyresume validate resumeA.json resumeB.json
```
FluentCV will validate each specified resume in turn:
HackMyResume will validate each specified resume in turn:
```bash
*** FluentCV v0.9.0 ***
*** HackMyResume v0.9.0 ***
Validating JSON resume: resumeA.json (INVALID)
Validating JSON resume: resumeB.json (VALID)
```
### Converting
FluentCV can convert between the [FRESH][fresca] and [JSON Resume][6] formats.
Just run:
HackMyResume can convert between the [FRESH][fresca] and [JSON Resume][6]
formats. Just run:
```bash
fluentcv CONVERT <INPUTS> <OUTPUTS>
hackmyresume CONVERT <INPUTS> <OUTPUTS>
```
where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and
<OUTPUTS> is a corresponding list of output file names. FluentCV will autodetect
the format (FRESH or JRS) of each input resume and convert it to the other
format (JRS or FRESH).
<OUTPUTS> is a corresponding list of output file names. HackMyResume will
autodetect the format (FRESH or JRS) of each input resume and convert it to the
other format (JRS or FRESH).
### Prettifying
FluentCV applies [js-beautify][10]-style HTML prettification by default to
HackMyResume applies [js-beautify][10]-style HTML prettification by default to
HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag
can be used:
```bash
fluentcv generate resume.json out.all --nopretty
hackmyresume generate resume.json out.all --nopretty
```
### Silent Mode
@ -265,8 +281,8 @@ fluentcv generate resume.json out.all --nopretty
Use `-s` or `--silent` to run in silent mode:
```bash
fluentcv generate resume.json -o someFile.all -s
fluentcv generate resume.json -o someFile.all --silent
hackmyresume generate resume.json -o someFile.all -s
hackmyresume generate resume.json -o someFile.all --silent
```
## License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

BIN
assets/hackmyresume_cli.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/resume-bouqet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

@ -1,10 +1,10 @@
{
"name": "fluentcv",
"version": "0.10.0",
"description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.",
"name": "hackmyresume",
"version": "1.2.1",
"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",
"url": "https://github.com/fluentdesk/fluentcv.git"
"url": "https://github.com/hacksalot/HackMyResume.git"
},
"keywords": [
"resume",
@ -18,33 +18,40 @@
"PDF",
"YAML",
"HTML",
"CLI"
"LaTeX",
"CLI",
"Handlebars",
"Underscore",
"template"
],
"author": "James M. Devlin",
"author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
"license": "MIT",
"preferGlobal": "true",
"bugs": {
"url": "https://github.com/fluentdesk/fluentcv/issues"
"url": "https://github.com/hacksalot/HackMyResume/issues"
},
"main": "src/fluentcmd.js",
"main": "src/hackmycmd.js",
"bin": {
"fluentcv": "src/index.js"
"hackmyresume": "src/index.js"
},
"homepage": "https://github.com/fluentdesk/fluentcv",
"homepage": "https://github.com/hacksalot/HackMyResume",
"dependencies": {
"fresca": "^0.2.0",
"colors": "^1.1.2",
"fluent-themes": "0.6.0-beta",
"fluent-themes": "~0.7.1-beta",
"fresca": "~0.2.2",
"fs-extra": "^0.24.0",
"handlebars": "^4.0.5",
"html": "0.0.10",
"is-my-json-valid": "^2.12.2",
"jst": "0.0.13",
"lodash": "^3.10.1",
"marked": "^0.3.5",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"moment": "^2.10.6",
"path-exists": "^2.1.0",
"recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.2.0",
"underscore": "^1.8.3",
"wkhtmltopdf": "^0.1.5",
"xml-escape": "^1.0.0",
@ -53,10 +60,11 @@
"devDependencies": {
"chai": "*",
"grunt": "*",
"grunt-contrib-clean": "^0.7.0",
"grunt-contrib-jshint": "^0.11.3",
"grunt-contrib-yuidoc": "^0.10.0",
"grunt-simple-mocha": "*",
"is-my-json-valid": "^2.12.2",
"jane-q-fullstacker": "fluentdesk/jane-q-fullstacker",
"mocha": "*",
"resample": "fluentdesk/resample"
}

View File

@ -1,6 +1,7 @@
/**
FRESH to JSON Resume conversion routiens.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module convert.js
*/
(function(){
@ -14,6 +15,7 @@ FRESH to JSON Resume conversion routiens.
/**
Convert from JSON Resume format to FRESH.
@method toFresh
*/
toFRESH: function( src, foreign ) {
@ -47,87 +49,15 @@ FRESH to JSON Resume conversion routiens.
address: src.basics.location.address
},
employment: {
history: src.work.map( function( job ) {
return {
position: job.position,
employer: job.company,
summary: job.summary,
current: (!job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current') || undefined,
start: job.startDate,
end: job.endDate,
url: job.website,
keywords: "",
highlights: job.highlights
};
})
},
education: {
history: src.education.map(function(edu){
return {
institution: edu.institution,
start: edu.startDate,
end: edu.endDate,
grade: edu.gpa,
curriculum: edu.courses,
url: edu.website || edu.url || null,
summary: null,
area: edu.area,
studyType: edu.studyType
};
})
},
service: {
history: src.volunteer.map(function(vol) {
return {
type: 'volunteer',
position: vol.position,
organization: vol.organization,
start: vol.startDate,
end: vol.endDate,
url: vol.website,
summary: vol.summary,
highlights: vol.highlights
};
})
},
employment: employment( src.work, true ),
education: education( src.education, true),
service: service( src.volunteer, true),
skills: skillsToFRESH( src.skills ),
writing: src.publications.map(function(pub){
return {
title: pub.name,
flavor: undefined,
publisher: pub.publisher,
url: pub.website,
date: pub.releaseDate,
summary: pub.summary
};
}),
recognition: src.awards.map(function(awd){
return {
title: awd.title,
date: awd.date,
summary: awd.summary,
from: awd.awarder,
url: null
};
}),
social: src.basics.profiles.map(function(pro){
return {
label: pro.network,
network: pro.network,
url: pro.url,
user: pro.username
};
}),
writing: writing( src.publications, true),
recognition: recognition( src.awards, true, foreign ),
social: social( src.basics.profiles, true ),
interests: src.interests,
references: src.references,
testimonials: references( src.references, true ),
languages: src.languages,
disposition: src.disposition // <--> round-trip
};
@ -160,77 +90,17 @@ FRESH to JSON Resume conversion routiens.
countryCode: src.location.country,
region: src.location.region
},
profiles: src.social.map(function(soc){
return {
network: soc.network,
username: soc.user,
url: soc.url
};
})
profiles: social( src.social, false )
},
work: src.employment.history.map(function(emp){
return {
company: emp.employer,
website: emp.url,
position: emp.position,
startDate: emp.start,
endDate: emp.end,
summary: emp.summary,
highlights: emp.highlights
};
}),
education: src.education.history.map(function(edu){
return {
institution: edu.institution,
gpa: edu.grade,
courses: edu.curriculum,
startDate: edu.start,
endDate: edu.end,
area: edu.area,
studyType: edu.studyType
};
}),
skills: skillsToJRS( src.skills ),
volunteer: src.service.history.map(function(srv){
return {
flavor: foreign ? srv.flavor : undefined,
organization: srv.organization,
position: srv.position,
startDate: srv.start,
endDate: srv.end,
website: srv.url,
summary: srv.summary,
highlights: srv.highlights
};
}),
awards: src.recognition.map(function(awd){
return {
flavor: foreign ? awd.flavor : undefined,
url: foreign ? awd.url: undefined,
title: awd.title,
date: awd.date,
awarder: awd.from,
summary: awd.summary
};
}),
publications: src.writing.map(function(pub){
return {
name: pub.title,
publisher: pub.publisher,
releaseDate: pub.date,
website: pub.url,
summary: pub.summary
};
}),
work: employment( src.employment, false ),
education: education( src.education, false ),
skills: skillsToJRS( src.skills, false ),
volunteer: service( src.service, false ),
awards: recognition( src.recognition, false, foreign ),
publications: writing( src.writing, false ),
interests: src.interests,
references: src.references,
references: references( src.testimonials, false ),
samples: foreign ? src.samples : undefined,
disposition: foreign ? src.disposition : undefined,
languages: src.languages
@ -250,6 +120,207 @@ FRESH to JSON Resume conversion routiens.
return obj;
}
function employment( obj, direction ) {
if( !direction ) {
return obj && obj.history ?
obj.history.map(function(emp){
return {
company: emp.employer,
website: emp.url,
position: emp.position,
startDate: emp.start,
endDate: emp.end,
summary: emp.summary,
highlights: emp.highlights
};
}) : undefined;
}
else {
return {
history: obj && obj.length ?
obj.map( function( job ) {
return {
position: job.position,
employer: job.company,
summary: job.summary,
current: (!job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current') || undefined,
start: job.startDate,
end: job.endDate,
url: job.website,
keywords: "",
highlights: job.highlights
};
}) : undefined
};
}
}
function education( obj, direction ) {
if( direction ) {
return obj && obj.length ? {
history: obj.map(function(edu){
return {
institution: edu.institution,
start: edu.startDate,
end: edu.endDate,
grade: edu.gpa,
curriculum: edu.courses,
url: edu.website || edu.url || null,
summary: null,
area: edu.area,
studyType: edu.studyType
};
})
} : undefined;
}
else {
return obj && obj.history ?
obj.history.map(function(edu){
return {
institution: edu.institution,
gpa: edu.grade,
courses: edu.curriculum,
startDate: edu.start,
endDate: edu.end,
area: edu.area,
studyType: edu.studyType
};
}) : undefined;
}
}
function service( obj, direction, foreign ) {
if( direction ) {
return {
history: obj && obj.length ? obj.map(function(vol) {
return {
type: 'volunteer',
position: vol.position,
organization: vol.organization,
start: vol.startDate,
end: vol.endDate,
url: vol.website,
summary: vol.summary,
highlights: vol.highlights
};
}) : undefined
};
}
else {
return obj && obj.history ?
obj.history.map(function(srv){
return {
flavor: foreign ? srv.flavor : undefined,
organization: srv.organization,
position: srv.position,
startDate: srv.start,
endDate: srv.end,
website: srv.url,
summary: srv.summary,
highlights: srv.highlights
};
}) : undefined;
}
}
function social( obj, direction ) {
if( direction ) {
return obj.map(function(pro){
return {
label: pro.network,
network: pro.network,
url: pro.url,
user: pro.username
};
});
}
else {
return obj.map( function( soc ) {
return {
network: soc.network,
username: soc.user,
url: soc.url
};
});
}
}
function recognition( obj, direction, foreign ) {
if( direction ) {
return obj && obj.length ? obj.map(
function(awd){
return {
flavor: foreign ? awd.flavor : undefined,
url: foreign ? awd.url: undefined,
title: awd.title,
date: awd.date,
from: awd.awarder,
summary: awd.summary
};
}) : undefined;
}
else {
return obj && obj.length ? obj.map(function(awd){
return {
flavor: foreign ? awd.flavor : undefined,
url: foreign ? awd.url: undefined,
title: awd.title,
date: awd.date,
awarder: awd.from,
summary: awd.summary
};
}) : undefined;
}
}
function references( obj, direction ) {
if( direction ) {
return obj && obj.length && obj.map(function(ref){
return {
name: ref.name,
flavor: 'professional',
quote: ref.reference,
private: false
};
});
}
else {
return obj && obj.length && obj.map(function(ref){
return {
name: ref.name,
reference: ref.quote
};
});
}
}
function writing( obj, direction ) {
if( direction ) {
return obj.map(function( pub ) {
return {
title: pub.name,
flavor: undefined,
publisher: pub.publisher,
url: pub.website,
date: pub.releaseDate,
summary: pub.summary
};
});
}
else {
return obj && obj.length ? obj.map(function(pub){
return {
name: pub.title,
publisher: pub.publisher && pub.publisher.name ? pub.publisher.name : pub.publisher,
releaseDate: pub.date,
website: pub.url,
summary: pub.summary
};
}) : undefined;
}
}
function skillsToFRESH( skills ) {
return {

View File

@ -0,0 +1,19 @@
(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: '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() }
];
}());

View File

@ -0,0 +1,13 @@
(function(){
module.exports = {
theme: 'modern',
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
unformatted: ['em','strong'],
max_char: 80, // ← See lib/html.js in above-linked repo
//wrap_line_length: 120, ← Don't use this
}
};
}());

View File

@ -47,6 +47,7 @@
"position": "",
"summary": "",
"start": "",
"end": "",
"keywords": [],
"highlights": []
}
@ -91,6 +92,7 @@
"sets": [
{
"name": "",
"level": "",
"skills": []
}
],

View File

@ -1,6 +1,7 @@
/**
The FluentCV date representation.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
The HackMyResume date representation.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module fluent-date.js
*/
var moment = require('moment');
@ -12,7 +13,7 @@ formats to be aware of here.
2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10")
3. Year-and-month only ("2015-04")
4. Year-only "YYYY" ("2015")
5. The friendly FluentCV "mmm YYYY" format ("Mar 2015" or "Dec 2008")
5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008")
6. Empty dates ("", " ")
7. Any other date format that Moment.js can parse from
Note: Moment can transparently parse all or most of these, without requiring us
@ -66,7 +67,10 @@ FluentDate/*.prototype*/.fmt = function( dt ) {
}
}
else {
if( dt.isValid && dt.isValid() )
if( !dt ) {
return moment();
}
else if( dt.isValid && dt.isValid() )
return dt;
throw 'Unknown date object encountered.';
}

View File

@ -1,6 +1,7 @@
/**
Definition of the FRESHResume class.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module fresh-resume.js
*/
(function() {
@ -9,10 +10,12 @@ Definition of the FRESHResume class.
, extend = require('../utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, __ = require('lodash')
, PATH = require('path')
, moment = require('moment')
, MD = require('marked')
, CONVERTER = require('./convert');
, CONVERTER = require('./convert')
, JRSResume = require('./jrs-resume');
/**
A FRESH-style resume in JSON or YAML.
@ -46,13 +49,14 @@ Definition of the FRESHResume class.
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
FreshResume.prototype.saveAs = function( filename, format ) {
this.imp.fileName = filename || this.imp.fileName;
if( format !== 'JRS' ) {
this.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
}
else {
var newRep = CONVERTER.toJRS( this );
FS.writeFileSync( this.imp.fileName, FreshResume.stringify( newRep ), 'utf8' );
FS.writeFileSync( filename, JRSResume.stringify( newRep ), 'utf8' );
}
return this;
};
@ -184,15 +188,6 @@ Definition of the FRESHResume class.
return flatSkills;
},
/**
Update the sheet's raw data. TODO: remove/refactor
*/
FreshResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str );
return this;
};
/**
Reset the sheet to an empty state.
*/
@ -211,6 +206,16 @@ Definition of the FRESHResume class.
delete this.social;
};
/**
Get a safe count of the number of things in a section.
*/
FreshResume.prototype.count = function( obj ) {
if( !obj ) return 0;
if( obj.history ) return obj.history.length;
if( obj.sets ) return obj.sets.length;
return obj.length || 0;
};
/**
Get the default (empty) sheet.
*/
@ -224,9 +229,18 @@ Definition of the FRESHResume class.
*/
FreshResume.prototype.add = function( moniker ) {
var defSheet = FreshResume.default();
var newObject = $.extend( true, {}, defSheet[ moniker ][0] );
var newObject = defSheet[moniker].history ?
$.extend( true, {}, defSheet[ moniker ].history[0] ) :
(moniker === 'skills' ?
$.extend( true, {}, defSheet.skills.sets[0] ) :
$.extend( true, {}, defSheet[ moniker ][0] ));
this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject );
if( this[ moniker ].history )
this[ moniker ].history.push( newObject );
else if( moniker === 'skills' )
this.skills.sets.push( newObject );
else
this[ moniker ].push( newObject );
return newObject;
};
@ -277,7 +291,7 @@ Definition of the FRESHResume class.
Validate the sheet against the FRESH Resume schema.
*/
FreshResume.prototype.isValid = function( info ) {
var schemaObj = require('FRESCA');
var schemaObj = require('fresca');
var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
@ -299,15 +313,17 @@ Definition of the FRESHResume class.
sheets that have overlapping jobs.
*/
FreshResume.prototype.duration = function() {
if( this.employment.history && this.employment.history.length ) {
var careerStart = this.employment.history[ this.employment.history.length - 1].safe.start;
var empHist = __.get(this, 'employment.history');
if( empHist && empHist.length ) {
var firstJob = _.last( this.employment.history );
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 ) {
return w.safe.end.unix();
}).safe.end;
return careerLast.diff( careerStart, 'years' );
return( w.safe && w.safe.end ) ? w.safe.end.unix() : moment().unix();
});
return careerLast.safe.end.diff( careerStart, 'years' );
}
return 0;
};
@ -318,9 +334,9 @@ Definition of the FRESHResume class.
*/
FreshResume.prototype.sort = function( ) {
this.employment.history && this.employment.history.sort( byDateDesc );
this.education.history && this.education.history.sort( byDateDesc );
this.service.history && this.service.history.sort( byDateDesc );
__.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 );
// this.awards && this.awards.sort( function(a, b) {
// return( a.safeDate.isBefore(b.safeDate) ) ? 1
@ -366,7 +382,7 @@ Definition of the FRESHResume class.
replaceDatesInObject( obj[key] );
});
['start','end','date'].forEach( function(val) {
if( obj[val] && (!obj.safe || !obj.safe[val] )) {
if( (obj[val] !== undefined) && (!obj.safe || !obj.safe[val] )) {
obj.safe = obj.safe || { };
obj.safe[ val ] = _fmt( obj[val] );
if( obj[val] && (val === 'start') && !obj.end ) {

View File

@ -1,6 +1,7 @@
/**
Definition of the JRSResume class.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module jrs-resume.js
*/
(function() {
@ -30,17 +31,22 @@ Definition of the JRSResume class.
consistent format. Then sort each section by startDate descending.
*/
JRSResume.prototype.open = function( file, title ) {
this.imp = { fileName: file };
this.imp.raw = FS.readFileSync( file, 'utf8' );
return this.parse( this.imp.raw, title );
//this.imp = { fileName: file }; <-- schema violation, tuck it into .basics instead
this.basics = {
imp: {
fileName: file,
raw: FS.readFileSync( file, 'utf8' )
}
};
return this.parse( this.basics.imp.raw, title );
};
/**
Save the sheet to disk (for environments that have disk access).
*/
JRSResume.prototype.save = function( filename ) {
this.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
this.basics.imp.fileName = filename || this.basics.imp.fileName;
FS.writeFileSync( this.basics.imp.fileName, this.stringify( this ), 'utf8' );
return this;
};
@ -48,7 +54,7 @@ Definition of the JRSResume class.
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
JRSResume.prototype.stringify = function() {
JRSResume.stringify = function( obj ) {
function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
@ -56,7 +62,11 @@ Definition of the JRSResume class.
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
return JSON.stringify( this, replacer, 2 );
return JSON.stringify( obj, replacer, 2 );
};
JRSResume.prototype.stringify = function() {
return JRSResume.stringify( this );
};
/**
@ -67,16 +77,17 @@ Definition of the JRSResume class.
JRSResume.prototype.parse = function( stringData, opts ) {
opts = opts || { };
var rep = JSON.parse( stringData );
extend( true, this, rep );
// Set up metadata
if( opts.imp === undefined || opts.imp ) {
this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.basics.name;
this.basics.imp = this.basics.imp || { };
this.basics.imp.title = (opts.title || this.basics.imp.title) || this.basics.name;
}
// Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this );
(opts.sort === undefined || opts.sort) && this.sort();
(opts.compute === undefined || opts.compute) && (this.computed = {
(opts.compute === undefined || opts.compute) && (this.basics.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
@ -96,22 +107,13 @@ Definition of the JRSResume class.
return flatSkills;
};
/**
Update the sheet's raw data. TODO: remove/refactor
*/
JRSResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str );
return this;
};
/**
Reset the sheet to an empty state.
*/
JRSResume.prototype.clear = function( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
clearMeta && (delete this.imp);
delete this.computed; // Don't use Object.keys() here
delete this.basics.computed; // Don't use Object.keys() here
delete this.work;
delete this.volunteer;
delete this.education;
@ -169,8 +171,15 @@ Definition of the JRSResume class.
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 );
return validate( this );
var validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
var ret = validate( this );
if( !ret ) {
this.basics.imp = this.basics.imp || { };
this.basics.imp.validationErrors = validate.errors;
}
return ret;
};
/**

View File

@ -0,0 +1,13 @@
(function(){
var FRESHResume = require('../core/fresh-resume');
module.exports = function loadSourceResumes( src, log, fn ) {
return src.map( function( res ) {
log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info +
res.cyan.bold );
return (fn && fn(res)) || (new FRESHResume()).open( res );
});
};
}());

View File

@ -1,6 +1,7 @@
/**
Abstract theme representation.
Definition of the Theme class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module theme.js
*/
(function() {
@ -15,7 +16,7 @@ Abstract theme representation.
, RECURSIVE_READ_DIR = require('recursive-readdir-sync');
/**
The Theme class is a representation of a FluentCV theme asset.
The Theme class is a representation of a HackMyResume theme asset.
@class Theme
*/
function Theme() {
@ -99,9 +100,19 @@ Abstract theme representation.
var outFmt = '', isMajor = false;
var portion = pathInfo.dir.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
if( portion[1] === '_' ) return;
var reg = /^(?:\/|\\)(html|latex|doc|pdf|partials)(?:\/|\\)?/ig;
var res = reg.exec( portion );
res && (outFmt = res[1]);
if( res ) {
if( res[1] !== 'partials' ) {
outFmt = res[1];
}
else {
that.partials = that.partials || [];
that.partials.push( { name: pathInfo.name, path: absPath } );
return null;
}
}
}
// Otherwise, the output format is inferred from the filename, as in
@ -123,7 +134,7 @@ Abstract theme representation.
action: 'transform',
path: absPath,
major: isMajor,
orgPath: PATH.relative(that.folder, absPath),
orgPath: PATH.relative(tplFolder, absPath),
ext: pathInfo.ext.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
@ -138,11 +149,11 @@ Abstract theme representation.
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; }))
(this.cssFiles = fmts.filter(function( fmt ){ return fmt && (fmt.ext === 'css'); }))
.forEach(function( cssf ) {
// For each CSS file, get its corresponding HTML file
var idx = _.findIndex(fmts, function( fmt ) {
return fmt.pre === cssf.pre && fmt.ext === 'html';
return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
});
cssf.action = null;
fmts[ idx ].css = cssf.data;
@ -151,7 +162,7 @@ Abstract theme representation.
// Remove CSS files from the formats array
fmts = fmts.filter( function( fmt) {
return fmt.ext !== 'css';
return fmt && (fmt.ext !== 'css');
});
return formatsHash;

169
src/eng/generic-helpers.js Normal file
View File

@ -0,0 +1,169 @@
/**
Generic template helper definitions for FluentCV.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module generic-helpers.js
*/
(function() {
var MD = require('marked')
, H2W = require('../utils/html-to-wpml')
, moment = require('moment')
, _ = require('underscore');
/**
Generic template helper function definitions.
@class GenericHelpers
*/
var GenericHelpers = module.exports = {
/**
Convert the input date to a specified format through Moment.js.
@method formatDate
*/
formatDate: function(datetime, format) {
return moment ? moment( datetime ).format( format ) : datetime;
},
/**
Convert inline Markdown to inline WordProcessingML.
@method wpml
*/
wpml: function( txt, inline ) {
if(!txt) return '';
inline = (inline && !inline.hash) || false;
txt = inline ?
MD(txt.trim()).replace(/^\s*<p>|<\/p>\s*$/gi, '') :
MD(txt.trim());
txt = H2W( txt.trim() );
return txt;
},
/**
Emit a conditional link.
@method link
*/
link: function( text, url ) {
return url && url.trim() ?
('<a href="' + url + '">' + text + '</a>') : text;
},
/**
Return the last word of the specified text.
@method lastWord
*/
lastWord: function( txt ) {
return txt && txt.trim() ? _.last( txt.split(' ') ) : '';
},
/**
Convert a skill level to an RGB color triplet.
@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
integer ("1", "5", etc.), or as an RRGGBB color triplet ('#C00000',
'#FFFFAA').
*/
skillColor: function( lvl ) {
var idx = skillLevelToIndex( lvl );
var skillColors = (this.theme && this.theme.palette &&
this.theme.palette.skillLevels) ||
[ '#FFFFFF', '#5CB85C', '#F1C40F', '#428BCA', '#C00000' ];
return skillColors[idx];
},
/**
Return an appropriate height.
@method lastWord
*/
skillHeight: function( lvl ) {
var idx = skillLevelToIndex( lvl );
return ['38.25', '30', '16', '8', '0'][idx];
},
/**
Return all but the last word of the input text.
@method initialWords
*/
initialWords: function( txt ) {
return txt && txt.trim() ? _.initial( txt.split(' ') ).join(' ') : '';
},
/**
Trim the protocol (http or https) from a URL/
@method trimURL
*/
trimURL: function( url ) {
return url && url.trim() ? url.trim().replace(/^https?:\/\//i, '') : '';
},
/**
Convert text to lowercase.
@method toLower
*/
toLower: function( txt ) {
return txt && txt.trim() ? txt.toLowerCase() : '';
},
/**
Return true if either value is truthy.
@method either
*/
either: function( lhs, rhs, options ) {
if (lhs || rhs) return options.fn(this);
},
/**
Perform a generic comparison.
See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates
@method compare
*/
compare: function(lvalue, rvalue, options) {
if (arguments.length < 3)
throw new Error("Handlerbars Helper 'compare' needs 2 parameters");
var operator = options.hash.operator || "==";
var operators = {
'==': function(l,r) { return l == r; },
'===': function(l,r) { return l === r; },
'!=': function(l,r) { return l != r; },
'<': function(l,r) { return l < r; },
'>': function(l,r) { return l > r; },
'<=': function(l,r) { return l <= r; },
'>=': function(l,r) { return l >= r; },
'typeof': function(l,r) { return typeof l == r; }
};
if (!operators[operator])
throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator);
var result = operators[operator](lvalue,rvalue);
return result ? options.fn(this) : options.inverse(this);
}
};
function skillLevelToIndex( lvl ) {
var idx = 0;
if( String.is( lvl ) ) {
lvl = lvl.trim().toLowerCase();
var intVal = parseInt( lvl );
if( isNaN( intVal ) ) {
switch( lvl ) {
case 'beginner': idx = 1; break;
case 'intermediate': idx = 2; break;
case 'advanced': idx = 3; break;
case 'master': idx = 4; break;
}
}
else {
idx = Math.min( intVal / 2, 4 );
idx = Math.max( 0, idx );
}
}
else {
idx = Math.min( lvl / 2, 4 );
idx = Math.max( 0, idx );
}
return idx;
}
}());

View File

@ -1,17 +1,49 @@
/**
Handlebars template generate for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
Definition of the HandlebarsGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module handlebars-generator.js
*/
(function() {
var _ = require('underscore');
var HANDLEBARS = require('handlebars');
module.exports = function( json, jst, format, cssInfo, opts ) {
var template = HANDLEBARS.compile(jst);
return template( { r: json, filt: opts.filters, cssInfo: cssInfo, headFragment: opts.headFragment || '' } );
var _ = require('underscore')
, HANDLEBARS = require('handlebars')
, FS = require('fs')
, registerHelpers = require('./handlebars-helpers');
/**
Perform template-based resume generation using Handlebars.js.
@class HandlebarsGenerator
*/
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.
registerHelpers( theme );
// Compile and run the Handlebars template.
var template = HANDLEBARS.compile(jst);
return template({
r: format === 'html' || format === 'pdf' ? json.markdownify() : json,
RAW: json,
filt: opts.filters,
cssInfo: cssInfo,
headFragment: opts.headFragment || ''
});
}
};

View File

@ -0,0 +1,25 @@
/**
Template helper definitions for Handlebars.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module handlebars-helpers.js
*/
(function() {
var HANDLEBARS = require('handlebars')
, _ = require('underscore')
, helpers = require('./generic-helpers');
/**
Register useful Handlebars helpers.
@method registerHelpers
*/
module.exports = function( theme ) {
helpers.theme = theme;
HANDLEBARS.registerHelper( helpers );
};
}());

View File

@ -1,37 +1,52 @@
/**
Underscore template generate for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
Definition of the UnderscoreGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module underscore-generator.js
*/
(function() {
var _ = require('underscore');
module.exports = function( json, jst, format, cssInfo, opts ) {
// Tweak underscore's default template delimeters
var delims = opts.themeObj.delimeters || opts.template;
if( opts.themeObj.delimeters ) {
delims = _.mapObject( delims, function(val,key) {
return new RegExp( val, "ig");
/**
Perform template-based resume generation using Underscore.js.
@class UnderscoreGenerator
*/
var UnderscoreGenerator = module.exports = {
generate: function( json, jst, format, cssInfo, opts, theme ) {
// Tweak underscore's default template delimeters
var delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template;
if( opts.themeObj && opts.themeObj.delimeters ) {
delims = _.mapObject( delims, function(val,key) {
return new RegExp( val, "ig");
});
}
_.templateSettings = delims;
// Strip {# comments #}
jst = jst.replace( delims.comment, '');
// Compile and run the template. TODO: avoid unnecessary recompiles.
var compiled = _.template(jst);
var ret = compiled({
r: format === 'html' || format === 'pdf' ? json.markdownify() : json,
filt: opts.filters,
XML: require('xml-escape'),
RAW: json,
cssInfo: cssInfo,
headFragment: opts.headFragment || ''
});
return ret;
}
_.templateSettings = delims;
// Strip {# comments #}
jst = jst.replace( delims.comment, '');
// Compile and run the template. TODO: avoid unnecessary recompiles.
var compiled = _.template(jst);
var ret = compiled({
r: format === 'html' || format === 'pdf' ? json.markdownify() : json,
filt: opts.filters,
XML: require('xml-escape'),
RAW: json,
cssInfo: cssInfo,
headFragment: opts.headFragment || ''
});
return ret;
};
}());

View File

@ -1,348 +0,0 @@
/**
Internal resume generation logic for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@module fluentcmd.js
*/
(function() {
module.exports = function () {
var path = require( 'path' )
, extend = require( './utils/extend' )
, unused = require('./utils/string')
, FS = require('fs')
, _ = require('underscore')
, FLUENT = require('./fluentlib')
, PATH = require('path')
, MKDIRP = require('mkdirp')
//, COLORS = require('colors')
, rez, _log, _err;
/**
Given a source JSON resume, a destination resume path, and a theme file,
generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
*/
function generate( src, dst, opts, logger, errHandler ) {
_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;
// Load input resumes...
if(!src || !src.length) { throw { fluenterror: 3 }; }
var sheets = loadSourceResumes( src );
// Merge input resumes...
var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ?
'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName;
return extend( true, b, a );
});
msg && _log(msg);
// Verify the specified theme name/path
var relativeThemeFolder = '../node_modules/fluent-themes/themes';
var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme);
var exists = require('./utils/file-exists');
if (!exists( tFolder )) {
tFolder = PATH.resolve( _opts.theme );
if (!exists( tFolder )) {
throw { fluenterror: 1, data: _opts.theme };
}
}
// Load the theme
var theTheme = new FLUENT.Theme().open( tFolder );
_opts.themeObj = theTheme;
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
(' theme (' +Object.keys(theTheme.formats).length + ' formats)').info);
// Expand output resumes... (can't use map() here)
var targets = [], that = this;
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) {
var to = path.resolve(t),
pa = path.parse(to),
fmat = pa.ext || '.all';
targets.push.apply(targets, fmat === '.all' ?
Object.keys( theTheme.formats ).map(function(k){
var z = theTheme.formats[k];
return { file: to.replace(/all$/g,z.outFormat), fmt: z };
}) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
});
// Run the transformation!
var finished = targets.map( function(t) { return single(t, theTheme); });
// Don't send the client back empty-handed
return { sheet: rez, targets: targets, processed: finished };
}
/**
Generate a single resume of a specific format.
@param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function single( targInfo, theme ) {
try {
var f = targInfo.file
, fType = targInfo.fmt.outFormat
, fName = path.basename(f, '.' + fType)
, theFormat;
// If targInfo.fmt.files exists, this theme has an explicit "files"
// section in its theme.json file.
if( targInfo.fmt.files && targInfo.fmt.files.length ) {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + path.relative(process.cwd(), f ).useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
// targInfo.fmt.files.forEach( function( form ) {
//
// if( form.action === 'transform' ) {
// var theFormat = _fmts.filter( function( fmt ) {
// return fmt.name === targInfo.fmt.outFormat;
// })[0];
// MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
// theFormat.gen.generate( rez, f, _opts );
// }
// else if( form.action === null ) {
// // Copy the file
// }
//
// });
}
// Otherwise the theme has no files section
else {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + path.relative(process.cwd(), f ).useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
}
}
catch( ex ) {
_err( ex );
}
}
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
/**
Validate 1 to N resumes in either FRESH or JSON Resume format.
*/
function validate( src, unused, opts, logger ) {
_log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
var isValid = true;
var validator = require('is-my-json-valid');
var schemas = {
fresh: require('FRESCA'),
jars: require('./core/resume.json')
};
// Load input resumes...
var sheets = loadSourceResumes(src, function( res ) {
try {
return {
file: res,
raw: FS.readFileSync( res, 'utf8' )
};
}
catch( ex ) {
throw ex;
}
});
sheets.forEach( function( rep ) {
var rez;
try {
rez = JSON.parse( rep.raw );
}
catch( ex ) {
_log('Validating '.info + rep.file.infoBold +
' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold);
if (ex instanceof SyntaxError) {
// Invalid JSON
_log( '--> '.bold.red + rep.file.toUpperCase().red +
' contains invalid JSON. Unable to validate.'.red );
_log( (' INTERNAL: ' + ex).red );
}
else {
_log(('ERROR: ' + ex.toString()).red.bold);
}
return;
}
var isValid = false;
var style = 'useful';
var errors = [];
var fmt = rez.meta &&
(rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars';
try {
var validate = validator( schemas[ fmt ], { // Note [1]
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
isValid = validate( rez );
if( !isValid ) {
style = 'warn';
errors = validate.errors;
}
}
catch(ex) {
}
_log( 'Validating '.info + rep.file.infoBold + ' against '.info +
fmt.replace('jars','JSON Resume').toUpperCase().infoBold +
' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold );
errors.forEach(function(err,idx) {
_log( '--> '.bold.yellow +
(err.field.replace('data.','resume.').toUpperCase() + ' ' +
err.message).yellow );
});
});
}
/**
Convert between FRESH and JRS formats.
*/
function convert( src, dst, opts, logger ) {
_log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
if( !dst || !dst.length ) {
if( src.length === 1 ) { throw { fluenterror: 5 }; }
else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; }
else { throw { fluenterror: 5 }; }
}
if( src && dst && src.length && dst.length && src.length !== dst.length ) {
throw { fluenterror: 7 };
}
var sheets = loadSourceResumes( src );
sheets.forEach(function(sheet, idx){
var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH';
var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS';
_log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' +
sourceFormat + ') to ').useful + dst[0].useful.bold +
(' (' + targetFormat + ').').useful );
sheet.saveAs( dst[idx], targetFormat );
});
}
/**
Create a new empty resume in either FRESH or JRS format.
*/
function create( src, dst, opts, logger ) {
_log = logger || console.log;
dst = src || ['resume.json'];
dst.forEach( function( t ) {
var safeFormat = opts.format.toUpperCase();
_log('Creating new '.useful +safeFormat.useful.bold +
' resume: '.useful + t.useful.bold);
MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists;
FLUENT[ safeFormat + 'Resume' ].default().save( t );
});
}
/**
Display help documentation.
*/
function help() {
console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' )
.useful.bold );
}
function loadSourceResumes( src, fn ) {
return src.map( function( res ) {
_log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info +
res.cyan.bold );
return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res );
});
}
/**
Supported resume formats.
*/
var _fmts = [
{ 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: '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() }
];
/**
Default FluentCV options.
*/
var _opts = {
theme: 'modern',
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
unformatted: ['em','strong'],
max_char: 80, // ← See lib/html.js in above-linked repo
//wrap_line_length: 120, ← Don't use this
}
};
/**
Internal module interface. Used by FCV Desktop and HMR.
*/
return {
verbs: {
build: generate,
validate: validate,
convert: convert,
new: create,
help: help
},
lib: require('./fluentlib'),
options: _opts,
formats: _fmts
};
}();
}());
// [1]: JSON.parse throws SyntaxError on invalid JSON. See:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse

View File

@ -1,6 +1,7 @@
/**
Base resume generator for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
Definition of the BaseGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module base-generator.js
*/
(function() {

View File

@ -1,6 +1,7 @@
/**
HTML resume generator for FluentCV.
@license Copyright (c) 2015 James M. Devlin / FluentDesk
Definition of the HTMLGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module html-generator.js
*/
(function() {
@ -21,16 +22,6 @@ HTML resume generator for FluentCV.
the HTML resume prior to saving.
*/
onBeforeSave: function( info ) {
var cssSrc = PATH.join( info.theme.folder, 'src', '*.css' )
, outFolder = PATH.parse( info.outputFile ).dir, that = this;
info.theme.cssFiles.forEach( function( f ) {
var fi = PATH.parse( f.path );
FS.copySync( f.path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) {
throw { fluenterror: that.codes.copyCss, data: [cssSrc,cssDst] };
});
});
return this.opts.prettify ?
HTML.prettyPrint( info.mk, this.opts.prettify ) : info.mk;
}

View File

@ -1,6 +1,7 @@
/**
Definition of the HtmlPdfGenerator class.
@license Copyright (c) 2015 James M. Devlin / FluentDesk
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module html-pdf-generator.js
*/
(function() {
@ -10,7 +11,7 @@ Definition of the HtmlPdfGenerator class.
, HTML = require( 'html' );
/**
An HTML-based PDF resume generator for FluentCV.
An HTML-based PDF resume generator for HackMyResume.
*/
var HtmlPdfGenerator = module.exports = TemplateGenerator.extend({
@ -22,8 +23,8 @@ Definition of the HtmlPdfGenerator class.
Generate the binary PDF.
*/
onBeforeSave: function( info ) {
pdf(info.mk, info.outputFile);
return info.mk;
pdf( info.mk, info.outputFile );
return null; // halt further processing
}
});

View File

@ -1,6 +1,7 @@
/**
Definition of the JsonGenerator class.
@license Copyright (c) 2015 | James M. Devlin
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module json-generator.js
*/
var BaseGenerator = require('./base-generator');

View File

@ -1,7 +1,7 @@
/**
A JSON-driven YAML resume generator for FluentLib.
Definition of the JsonYamlGenerator class.
@module json-yaml-generator.js
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
*/
(function() {

View File

@ -1,6 +1,7 @@
/**
LaTeX resume generator for FluentCV.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
Definition of the LaTeXGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module latex-generator.js
*/
var TemplateGenerator = require('./template-generator');

View File

@ -1,6 +1,7 @@
/**
Markdown resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the MarkdownGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module markdown-generator.js
*/
var TemplateGenerator = require('./template-generator');

View File

@ -1,6 +1,7 @@
/**
Template-based resume generator base for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
Definition of the TemplateGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module template-generator.js
*/
(function() {
@ -37,7 +38,9 @@ Template-based resume generator base for FluentCV.
raw: function( txt ) { return txt; },
xml: function( txt ) { return XML(txt); },
md: function( txt ) { return MD( txt || '' ); },
mdin: function( txt ) { return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); },
mdin: function( txt ) {
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
},
lower: function( txt ) { return txt.toLowerCase(); },
link: function( name, url ) { return url ?
'<a href="' + url + '">' + name + '</a>' : name; }
@ -68,24 +71,14 @@ Template-based resume generator base for FluentCV.
},
invoke: function( rez, themeMarkup, cssInfo, opts ) {
this.opts = EXTEND( true, {}, _defaultOpts, opts );
mk = this.single( rez, themeMarkup, this.format, cssInfo, { } );
this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f ));
return mk;
},
/**
Default generation method for template-based generators.
@method generate
String-based template generation method.
@method invoke
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options.
@returns An array of strings representing generated output files.
*/
generate: function( rez, f, opts ) {
invoke: function( rez, opts ) {
// Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts );
@ -95,20 +88,81 @@ Template-based resume generator base for FluentCV.
var theme = themeInfo.theme;
var tFolder = themeInfo.folder;
var tplFolder = PATH.join( tFolder, 'src' );
var curFmt = theme.getFormat( this.format );
var that = this;
// "Generate": process individual files within the theme
return {
files: curFmt.files.map( function( tplInfo ) {
return {
info: tplInfo,
data: tplInfo.action === 'transform' ?
transform.call( that, rez, tplInfo, theme ) : undefined
};
}).filter(function(item){ return item !== null; }),
themeInfo: themeInfo
};
},
/**
File-based template generation method.
@method generate
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options.
*/
generate: function( rez, f, opts ) {
// Call the generation method
var genInfo = this.invoke( rez, opts );
// Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts );
// Load the theme
var themeInfo = genInfo.themeInfo;
var theme = themeInfo.theme;
var tFolder = themeInfo.folder;
var tplFolder = PATH.join( tFolder, 'src' );
var outFolder = PATH.parse(f).dir;
var curFmt = theme.getFormat( this.format );
var that = this;
// "Generate": process individual files within the theme
curFmt.files.forEach(function(tplInfo){
if( tplInfo.action === 'transform' ) {
transform.call( that, rez, f, tplInfo, theme, outFolder );
genInfo.files.forEach(function( file ){
var thisFilePath;
if( file.info.action === 'transform' ) {
thisFilePath = PATH.join( outFolder, file.info.orgPath );
try {
if( that.onBeforeSave ) {
file.data = that.onBeforeSave({
theme: theme,
outputFile: (file.info.major ? f : thisFilePath),
mk: file.data
});
if( !file.data ) return; // PDF etc
}
var fileName = file.info.major ? f : thisFilePath;
MKDIRP.sync( PATH.dirname( fileName ) );
FS.writeFileSync( fileName, file.data,
{ encoding: 'utf8', flags: 'w' } );
that.onAfterSave && that.onAfterSave(
{ outputFile: fileName, mk: file.data } );
}
catch(ex) {
console.log(ex);
}
}
else if( tplInfo.action === null && theme.explicit ) {
var thisFilePath = PATH.join(outFolder, tplInfo.orgPath);
else if( file.info.action === null/* && theme.explicit*/ ) {
thisFilePath = PATH.join( outFolder, file.info.orgPath );
try {
MKDIRP.sync( PATH.dirname(thisFilePath) );
FS.copySync( tplInfo.path, thisFilePath );
FS.copySync( file.info.path, thisFilePath );
}
catch(ex) {
console.log(ex);
@ -121,7 +175,8 @@ Template-based resume generator base for FluentCV.
Object.keys( curFmt.symLinks ).forEach( function(loc) {
var absLoc = PATH.join(outFolder, loc);
var absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
var type = PATH.parse( absLoc ).ext ? 'file' : 'junction'; // 'file', 'dir', or 'junction' (Windows only)
// 'file', 'dir', or 'junction' (Windows only)
var type = PATH.parse( absLoc ).ext ? 'file' : 'junction';
FS.symlinkSync( absTarg, absLoc, type);
});
}
@ -138,10 +193,12 @@ Template-based resume generator base for FluentCV.
@param cssInfo Needs to be refactored.
@param opts Options and passthrough data.
*/
single: function( json, jst, format, cssInfo, opts ) {
single: function( json, jst, format, cssInfo, opts, theme ) {
this.opts.freezeBreaks && ( jst = freeze(jst) );
var eng = require( '../eng/' + opts.themeObj.engine + '-generator' );
var result = eng( json, jst, format, cssInfo, opts );
var eng = require( '../eng/' + theme.engine + '-generator' );
var result = eng.generate( json, jst, format, cssInfo, opts, theme );
this.opts.freezeBreaks && ( result = unfreeze(result) );
return result;
}
@ -167,7 +224,7 @@ Template-based resume generator base for FluentCV.
PATH.parse( require.resolve('fluent-themes') ).dir,
this.opts.theme
);
var exists = require('../utils/file-exists');
var exists = require('path-exists').sync;
if( !exists( tFolder ) ) {
tFolder = PATH.resolve( this.opts.theme );
if( !exists( tFolder ) ) {
@ -186,17 +243,15 @@ Template-based resume generator base for FluentCV.
/**
Transform a single subfile.
*/
function transform( rez, f, tplInfo, theme, outFolder ) {
var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null };
var mk = this.single( rez, tplInfo.data, this.format, cssInfo, this.opts );
this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } ));
var thisFilePath = PATH.join( outFolder, tplInfo.orgPath );
function transform( rez, tplInfo, theme ) {
try {
MKDIRP.sync( PATH.dirname( tplInfo.major ? f : thisFilePath) );
FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } );
var cssInfo = {
file: tplInfo.css ? tplInfo.cssPath : null,
data: tplInfo.css || null
};
return this.single( rez, tplInfo.data, this.format, cssInfo, this.opts,
theme );
}
catch(ex) {
console.log(ex);

View File

@ -1,6 +1,7 @@
/**
Plain text resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the TextGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module text-generator.js
*/
var TemplateGenerator = require('./template-generator');

View File

@ -1,13 +1,19 @@
/**
MS Word resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the WordGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module word-generator.js
*/
var TemplateGenerator = require('./template-generator');
var WordGenerator = module.exports = TemplateGenerator.extend({
(function() {
init: function(){
this._super( 'doc', 'xml' );
},
var TemplateGenerator = require('./template-generator');
var WordGenerator = module.exports = TemplateGenerator.extend({
});
init: function(){
this._super( 'doc', 'xml' );
}
});
}());

View File

@ -1,6 +1,7 @@
/**
XML resume generator for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
Definition of the XMLGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module xml-generator.js
*/
var BaseGenerator = require('./base-generator');
@ -8,7 +9,7 @@ var BaseGenerator = require('./base-generator');
/**
The XmlGenerator generates an XML resume via the TemplateGenerator.
*/
var XmlGenerator = module.exports = BaseGenerator.extend({
var XMLGenerator = module.exports = BaseGenerator.extend({
init: function(){
this._super( 'xml' );

View File

@ -1,7 +1,7 @@
/**
A YAML resume generator for FluentLib.
Definition of the YAMLGenerator class.
@module yaml-generator.js
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
*/
@ -13,7 +13,7 @@ A YAML resume generator for FluentLib.
YamlGenerator generates a YAML-formatted resume via TemplateGenerator.
*/
var YamlGenerator = module.exports = TemplateGenerator.extend({
var YAMLGenerator = module.exports = TemplateGenerator.extend({
init: function(){
this._super( 'yml', 'yml' );

View File

@ -1,6 +1,7 @@
/**
External API surface for FluentCV:CLI.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
External API surface for HackMyResume.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
@module hackmyapi.js
*/
module.exports = {

45
src/hackmycmd.js Normal file
View File

@ -0,0 +1,45 @@
/**
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');
/**
Display help documentation.
*/
function help() {
console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' )
.useful.bold );
}
/**
Internal module interface. Used by FCV Desktop and HMR.
*/
return {
verbs: {
generate: require('./verbs/generate'),
build: require('./verbs/generate'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
create: require('./verbs/create'),
new: require('./verbs/create'),
help: help
},
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,18 +1,19 @@
#! /usr/bin/env node
/**
Command-line interface (CLI) for FluentCV:CLI.
Command-line interface (CLI) for HackMyResume.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
@module index.js
*/
var ARGS = require( 'minimist' )
, FCMD = require( './fluentcmd')
, FCMD = require( './hackmycmd')
, PKG = require('../package.json')
, COLORS = require('colors')
, FS = require('fs')
, PATH = require('path')
, opts = { }
, title = ('*** FluentCV v' + PKG.version + ' ***').bold.white
, title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white
, _ = require('underscore');
@ -46,7 +47,6 @@ function main() {
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];
@ -55,7 +55,7 @@ function main() {
return;
}
// Get source and dest params
// Find the TO keyword, if any
var splitAt = _.indexOf( params, 'to' );
if( splitAt === a._.length - 1 ) {
// 'TO' cannot be the last argument
@ -65,8 +65,10 @@ function main() {
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 && dst.push( src.pop() ); // Allow omitting TO keyword
var parms = [ src, dst, opts, logMsg ];
// Invoke the action
@ -106,9 +108,10 @@ function handleError( ex ) {
}).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold;
break;
//case 4: msg = title + '\n' + ; break;
case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break;
case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created.'.guide; break;
case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break;
case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break;
case 8: msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold + ' to create.'.guide; break;
}
exitCode = ex.fluenterror;

View File

@ -1,6 +1,6 @@
Usage:
fluentcv <COMMAND> <SOURCES> [TO <TARGETS>] [-t <THEME>] [-f <FORMAT>]
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>
@ -10,16 +10,16 @@ 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).
fluentcv BUILD resume.json TO out/resume.all
fluentcv NEW resume.json
fluentcv CONVERT resume.json TO resume-jrs.json
fluentcv VALIDATE resume.json
hackmyresume BUILD resume.json TO out/resume.all
hackmyresume NEW resume.json
hackmyresume CONVERT resume.json TO resume-jrs.json
hackmyresume VALIDATE resume.json
Both SOURCES and TARGETS can accept multiple files:
fluentCV BUILD r1.json r2.json TO out/resume.all out2/resume.html
fluentCV NEW r1.json r2.json r3.json
fluentCV VALIDATE resume.json resume2.json resume3.json
hackmyresume BUILD r1.json r2.json TO out/resume.all out2/resume.html
hackmyresume NEW r1.json r2.json r3.json
hackmyresume VALIDATE resume.json resume2.json resume3.json
See https://github.com/fluentdesk/fluentCV/blob/master/README.md for more
See https://github.com/hacksalot/hackmyresume/blob/master/README.md for more
information.

View File

@ -1,3 +1,8 @@
/**
Definition of John Resig's `Class` class.
@module class.js
*/
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.

View File

@ -1,6 +1,7 @@
/**
Plain JavaScript replacement of jQuery .extend based on jQuery sources.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the `extend` method.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module extend.js
*/
function _extend() {

View File

@ -1,18 +0,0 @@
/**
File-exists checker for Node.js.
@license Copyright (c) 2015 | James M. Devlin
*/
var FS = require('fs');
// Yup, this is now the recommended way to check for file existence on Node.
// fs.exists is deprecated and the recommended fs.statSync/lstatSync throws
// exceptions on non-existent paths :)
module.exports = function (path) {
try {
FS.statSync( path );
return true;
} catch( err ) {
return !(err && err.code === 'ENOENT');
}
};

64
src/utils/html-to-wpml.js Normal file
View File

@ -0,0 +1,64 @@
/**
Definition of the Markdown to WordProcessingML conversion routine.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module html-to-wpml.js
*/
(function(){
var _ = require('underscore');
var HTML5Tokenizer = require('simple-html-tokenizer');
module.exports = function( html ) {
// Tokenize the HTML stream.
var tokens = HTML5Tokenizer.tokenize( html );
var final = '', is_bold, is_italic, is_link, link_url;
// Process <em>, <strong>, and <a> elements in the HTML stream, producing
// equivalent WordProcessingML that can be dumped into a <w:p> or other
// text container element.
_.each( tokens, function( tok ) {
switch( tok.type ) {
case 'StartTag':
switch( tok.tagName ) {
case 'p': final += '<w:p>'; break;
case 'strong': is_bold = true; break;
case 'em': is_italic = true; break;
case 'a':
is_link = true;
link_url = tok.attributes.filter(function(attr){
return attr[0] === 'href'; }
)[0][1];
break;
}
break;
case 'EndTag':
switch( tok.tagName ) {
case 'p': final += '</w:p>'; break;
case 'strong': is_bold = false; break;
case 'em': is_italic = false; break;
case 'a': is_link = false; break;
}
break;
case 'Chars':
var style = is_bold ? '<w:b/>' : '';
style += is_italic ? '<w:i/>': '';
style += is_link ? '<w:rStyle w:val="Hyperlink"/>' : '';
final +=
(is_link ? ('<w:hlink w:dest="' + link_url + '">') : '') +
'<w:r><w:rPr>' + style + '</w:rPr><w:t>' + tok.chars +
'</w:t></w:r>' + (is_link ? '</w:hlink>' : '');
break;
}
});
return final;
};
}());

View File

@ -1,6 +1,7 @@
/**
String utility functions.
@license Copyright (c) 2015 | James M. Devlin
Definitions of string utility functions.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module string.js
*/
/**
@ -16,3 +17,7 @@ String.isNullOrWhitespace = function( input ) {
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
String.is = function( val ) {
return typeof val === 'string' || val instanceof String;
};

30
src/verbs/convert.js Normal file
View File

@ -0,0 +1,30 @@
(function(){
var loadSourceResumes = require('../core/load-source-resumes');
/**
Convert between FRESH and JRS formats.
*/
module.exports = function convert( src, dst, opts, logger ) {
var _log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
if( !dst || !dst.length ) {
if( src.length === 1 ) { throw { fluenterror: 5 }; }
else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; }
else { throw { fluenterror: 5 }; }
}
if( src && dst && src.length && dst.length && src.length !== dst.length ) {
throw { fluenterror: 7 };
}
var sheets = loadSourceResumes( src, _log );
sheets.forEach(function(sheet, idx){
var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH';
var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS';
_log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' +
sourceFormat + ') to ').useful + dst[0].useful.bold +
(' (' + targetFormat + ').').useful );
sheet.saveAs( dst[idx], targetFormat );
});
};
}());

22
src/verbs/create.js Normal file
View File

@ -0,0 +1,22 @@
(function(){
var FLUENT = require('../hackmyapi')
, MKDIRP = require('mkdirp')
, PATH = require('path');
/**
Create a new empty resume in either FRESH or JRS format.
*/
module.exports = function create( src, dst, opts, logger ) {
var _log = logger || console.log;
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);
MKDIRP.sync( PATH.dirname( t ) ); // Ensure dest folder exists;
FLUENT[ safeFormat + 'Resume' ].default().save( t );
});
};
}());

149
src/verbs/generate.js Normal file
View File

@ -0,0 +1,149 @@
(function() {
var PATH = require('path')
, MKDIRP = require('mkdirp')
, _opts = require('../core/default-options')
, FluentTheme = require('../core/theme')
, loadSourceResumes = require('../core/load-source-resumes')
, _ = require('underscore')
, _fmts = require('../core/default-formats')
, _err, _log, rez;
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
module.exports =
/**
Given a source JSON resume, a destination resume path, and a theme file,
generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
*/
function generate( src, dst, opts, logger, errHandler ) {
_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;
// Load input resumes...
if( !src || !src.length ) { throw { fluenterror: 3 }; }
var sheets = loadSourceResumes( src, _log );
// Merge input resumes...
var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ?
'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName;
return extend( true, b, a );
});
msg && _log(msg);
// Verify the specified theme name/path
var relativeThemeFolder = '../../node_modules/fluent-themes/themes';
var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme);
var exists = require('path-exists').sync;
if (!exists( tFolder )) {
tFolder = PATH.resolve( _opts.theme );
if (!exists( tFolder )) {
throw { fluenterror: 1, data: _opts.theme };
}
}
// Load the theme
var theTheme = new FluentTheme().open( tFolder );
_opts.themeObj = theTheme;
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
(' theme (' + Object.keys(theTheme.formats).length + ' formats)').info);
// Expand output resumes... (can't use map() here)
var targets = [], that = this;
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) {
var to = PATH.resolve(t),
pa = PATH.parse(to),
fmat = pa.ext || '.all';
targets.push.apply(targets, fmat === '.all' ?
Object.keys( theTheme.formats ).map(function(k){
var z = theTheme.formats[k];
return { file: to.replace(/all$/g,z.outFormat), fmt: z };
}) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
});
// Run the transformation!
var finished = targets.map( function(t) { return single(t, theTheme); });
// Don't send the client back empty-handed
return { sheet: rez, targets: targets, processed: finished };
};
/**
Generate a single resume of a specific format.
@param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function single( targInfo, theme ) {
try {
var f = targInfo.file
, fType = targInfo.fmt.outFormat
, fName = PATH.basename(f, '.' + fType)
, theFormat;
// If targInfo.fmt.files exists, this theme has an explicit "files"
// section in its theme.json file.
if( targInfo.fmt.files && targInfo.fmt.files.length ) {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
// targInfo.fmt.files.forEach( function( form ) {
//
// if( form.action === 'transform' ) {
// var theFormat = _fmts.filter( function( fmt ) {
// return fmt.name === targInfo.fmt.outFormat;
// })[0];
// MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
// theFormat.gen.generate( rez, f, _opts );
// }
// else if( form.action === null ) {
// // Copy the file
// }
//
// });
}
// Otherwise the theme has no files section
else {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
}
}
catch( ex ) {
_err( ex );
}
}
}());

97
src/verbs/validate.js Normal file
View File

@ -0,0 +1,97 @@
(function() {
var FS = require('fs');
var loadSourceResumes = require('../core/load-source-resumes');
module.exports =
/**
Validate 1 to N resumes in either FRESH or JSON Resume format.
*/
function validate( src, unused, opts, logger ) {
var _log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
var isValid = true;
var validator = require('is-my-json-valid');
var schemas = {
fresh: require('fresca'),
jars: require('../core/resume.json')
};
// Load input resumes...
var sheets = loadSourceResumes(src, _log, function( res ) {
try {
return {
file: res,
raw: FS.readFileSync( res, 'utf8' )
};
}
catch( ex ) {
throw ex;
}
});
sheets.forEach( function( rep ) {
var rez;
try {
rez = JSON.parse( rep.raw );
}
catch( ex ) { // Note [1]
_log('Validating '.info + rep.file.infoBold +
' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold);
if (ex instanceof SyntaxError) {
// Invalid JSON
_log( '--> '.bold.red + rep.file.toUpperCase().red +
' contains invalid JSON. Unable to validate.'.red );
_log( (' INTERNAL: ' + ex).red );
}
else {
_log(('ERROR: ' + ex.toString()).red.bold);
}
return;
}
var isValid = false;
var style = 'useful';
var errors = [];
var fmt = rez.meta &&
(rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars';
try {
var validate = validator( schemas[ fmt ], { // Note [1]
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
isValid = validate( rez );
if( !isValid ) {
style = 'warn';
errors = validate.errors;
}
}
catch(ex) {
return;
}
_log( 'Validating '.info + rep.file.infoBold + ' against '.info +
fmt.replace('jars','JSON Resume').toUpperCase().infoBold +
' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold );
errors.forEach(function(err,idx) {
_log( '--> '.bold.yellow +
(err.field.replace('data.','resume.').toUpperCase() + ' ' +
err.message).yellow );
});
});
};
}());

View File

@ -0,0 +1,91 @@
{
"basics": {
"name": "",
"label": "",
"picture": "",
"email": "",
"phone": "",
"degree": "",
"website": "",
"summary": "",
"location": {
"address": "",
"postalCode": "",
"city": "",
"countryCode": "",
"region": ""
},
"profiles": [
{
"network": "",
"username": "",
"url": ""
}
]
},
"work": [
{
"company": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [
""
]
}
],
"awards": [
{
"title": "",
"date": "",
"awarder": "",
"summary": ""
}
],
"education": [
{
"institution": "",
"area": "",
"studyType": "",
"startDate": "",
"endDate": "",
"gpa": "",
"courses": [
""
]
}
],
"publications": [
{
"name": "",
"publisher": "",
"releaseDate": "",
"website": "",
"summary": ""
}
],
"volunteer": [
{
"organization": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [
""
]
}
],
"skills": [
{
"name": "",
"level": "",
"keywords": [
""
]
}
]
}

View File

@ -0,0 +1,104 @@
{
"basics": {
"name": "Jane Q. Fullstacker"
},
"education": [
{
"gpa": "3.5",
"courses": [
"Course 1",
"Course 2",
"Course 2"
],
"startDate": "2005-09",
"endDate": "2008-05"
},
{
"institution": "Medfield College",
"gpa": "3.2",
"courses": [
"Course 1",
"Course 2",
"Course 2"
],
"startDate": "2003-09",
"endDate": "2005-06"
}
],
"skills": [
{
"name": "Web Dev",
"keywords": [
"JavaScript",
"HTML 5",
"CSS",
"LAMP",
"MVC",
"REST"
]
},
{
"name": "JavaScript"
}
],
"volunteer": [],
"publications": [
{
"name": "Building User Interfaces with Electron and Atom",
"releaseDate": "2011",
"website": "http://codeproject.com/build-ui-electron-atom.aspx"
},
{
"name": "Jane Fullstacker's Blog",
"publisher": "self",
"releaseDate": "2011",
"website": "http://janef.me"
},
{
"name": "Teach Yourself GORFF in 21 Days",
"publisher": "Amazon"
}
],
"interests": [
{
"name": "reading",
"summary": "Jane is a fan of mystery novels and courtroom dramas including Agatha Christie and John Grisham.",
"keywords": [
"mystery",
"Agatha Christie",
"John Grisham"
]
},
{
"name": "hiking",
"summary": "Jane enjoys hiking, light mountain climbing, and has four summits under her belt!"
},
{
"name": "yoga"
}
],
"references": [
{
"name": "John Davidson",
"reference": "Jane is awesome! I'd hire her again in a heartbeat."
},
{
"name": "Elias Fullstacker",
"reference": "I worked with Jane on Jabberwocky and can vouch for her awesome technical capabilities and attention to detail. Insta-hire."
},
{
"name": "Dana Nevins",
"reference": "I've known Jane personally and professionally for almost ten years. She is one in a million."
}
],
"languages": [
{
"language": "English",
"level": "Native"
},
{
"language": "Spanish",
"level": "Moderate"
}
]
}

View File

@ -0,0 +1,263 @@
{
"basics": {
"name": "Jane Q. Fullstacker",
"label": "Senior Developer",
"summary": "**Full-stack software developer with 6+ years industry experience** specializing in scalable cloud architectures for this, that, and the other. A native of southern CA, Jane enjoys hiking, mystery novels, and the company of Rufus, her two-year-old beagle.",
"website": "http://janef.me/blog",
"phone": "1-650-999-7777",
"email": "jdoe@onecoolstartup.io",
"picture": "jane_doe.png",
"location": {
"address": "Jane Fullstacker\n123 Somewhere Rd.\nMountain View, CA 94035",
"postalCode": "94035",
"city": "Mountain View",
"countryCode": "US",
"region": "CA"
},
"profiles": [
{
"network": "GitHub",
"username": "janef-was-here",
"url": "https://github.com/janef-was-here"
},
{
"network": "Twitter",
"username": "janef-was-here",
"url": "https://twitter.com/janef-was-here"
}
]
},
"work": [
{
"company": "One Cool Startup",
"website": "https://onecool.io/does-not-exist",
"position": "Head Code Ninja",
"startDate": "2013-09",
"summary": "Development team manager for OneCoolApp and OneCoolWebsite, a free social network tiddlywink generator and lifestyle portal with over 200,000 users.",
"highlights": [
"Managed a 5-person development team",
"Accomplishment 2",
"Etc."
]
},
{
"company": "Veridian Dynamics",
"website": "https://en.wikipedia.org/wiki/Better_Off_Ted#Plot",
"position": "Principal Developer",
"startDate": "2011-07",
"endDate": "2013-08",
"summary": "Developer on numerous projects culminating in technical lead role for the [Jabberwocky project](http://betteroffted.wikia.com/wiki/Jabberwocky) and promotion to principal developer.",
"highlights": [
"Managed a 5-person development team",
"Accomplishment 2",
"Etc."
]
},
{
"company": "Stark Industries",
"position": "IT Administrator",
"startDate": "2008-10",
"endDate": "2011-06",
"summary": "Junior programmer with heavy code responsibilities. Promoted to intermediate role after 6 months.",
"highlights": [
"Promoted to intermediate developer after 6 months",
"Accomplishment 2",
"Etc."
]
},
{
"company": "Dunder Mifflin",
"position": "Intern",
"startDate": "2008-06",
"endDate": "2008-09",
"summary": "Performed IT administration and deployments for Dunder Mifflin.",
"highlights": [
"Supervised roll-out of Dunder Mifflin Infinity website.",
"Performed mission-critical system backups and ",
"Etc."
]
}
],
"education": [
{
"institution": "Cornell University",
"gpa": "3.5",
"courses": [
"Course 1",
"Course 2",
"Course 2"
],
"startDate": "2005-09",
"endDate": "2008-05"
},
{
"institution": "Medfield College",
"gpa": "3.2",
"courses": [
"Course 1",
"Course 2",
"Course 2"
],
"startDate": "2003-09",
"endDate": "2005-06"
}
],
"skills": [
{
"name": "Web Dev",
"keywords": [
"JavaScript",
"HTML 5",
"CSS",
"LAMP",
"MVC",
"REST"
]
},
{
"name": "JavaScript",
"keywords": [
"Node.js",
"Angular.js",
"jQuery",
"Bootstrap",
"React.js",
"Backbone.js"
]
},
{
"name": "Database",
"keywords": [
"MySQL",
"PostgreSQL",
"NoSQL",
"ORM",
"Hibernate"
]
},
{
"name": "Cloud",
"keywords": [
"AWS",
"EC2",
"RDS",
"S3",
"Azure",
"Dropbox"
]
},
{
"name": "Project",
"keywords": [
"Agile",
"TFS",
"Unified Process",
"MS Project"
]
}
],
"volunteer": [
{
"organization": "Technology for Tots",
"position": "Technical Consultant",
"startDate": "2003-11",
"endDate": "2005-06",
"website": "http://technology-for-tots.org",
"summary": "Summary of this volunteer stint.",
"highlights": [
"Accomplishment 1",
"Accomplishment 2",
"etc"
]
},
{
"organization": "US Army Reserves",
"position": "NCO",
"startDate": "1999-11",
"endDate": "2003-06",
"website": "http://www.usar.army.mil/",
"summary": "Summary of this military stint.",
"highlights": [
"Accomplishment 1",
"Accomplishment 2",
"etc"
]
}
],
"awards": [
{
"title": "Honorable Mention",
"date": "2012",
"awarder": "Google"
},
{
"title": "Summa cum laude",
"date": "2012",
"awarder": "Cornell University"
}
],
"publications": [
{
"name": "Building User Interfaces with Electron and Atom",
"publisher": "Code Project",
"releaseDate": "2011",
"website": "http://codeproject.com/build-ui-electron-atom.aspx"
},
{
"name": "Jane Fullstacker's Blog",
"publisher": "self",
"releaseDate": "2011",
"website": "http://janef.me"
},
{
"name": "Teach Yourself GORFF in 21 Days",
"publisher": "Amazon",
"releaseDate": "2008",
"website": "http://url.to.publication.com/blah",
"summary": "A primer on the programming language of GORFF, whose for loops are coterminous with all of time and space."
}
],
"interests": [
{
"name": "reading",
"summary": "Jane is a fan of mystery novels and courtroom dramas including Agatha Christie and John Grisham.",
"keywords": [
"mystery",
"Agatha Christie",
"John Grisham"
]
},
{
"name": "hiking",
"summary": "Jane enjoys hiking, light mountain climbing, and has four summits under her belt!"
},
{
"name": "yoga"
}
],
"references": [
{
"name": "John Davidson",
"reference": "Jane is awesome! I'd hire her again in a heartbeat."
},
{
"name": "Elias Fullstacker",
"reference": "I worked with Jane on Jabberwocky and can vouch for her awesome technical capabilities and attention to detail. Insta-hire."
},
{
"name": "Dana Nevins",
"reference": "I've known Jane personally and professionally for almost ten years. She is one in a million."
}
],
"languages": [
{
"language": "English",
"level": "Native"
},
{
"language": "Spanish",
"level": "Moderate",
"years": 10
}
]
}

76
tests/test-cli.js Normal file
View File

@ -0,0 +1,76 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, FCMD = require( '../src/hackmycmd')
, validator = require('is-my-json-valid')
, COLORS = require('colors');
chai.config.includeStack = false;
describe('Testing CLI interface', function () {
var _sheet;
function logMsg() {
}
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
};
var opts2 = {
format: 'JRS',
prettify: true,
silent: true
};
run( 'new', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
run( 'new', ['tests/sandbox/new-1.json', 'tests/sandbox/new-2.json', 'tests/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' );
run( 'new', ['tests/sandbox/new-jrs-1.json', 'tests/sandbox/new-jrs-2.json', 'tests/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' );
run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
fail( 'new', [], [], opts, " (when a filename isn't specified)" );
run( 'validate', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], [], opts, ' (FRESH format)' );
run( 'validate', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
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 );
}
runIt.should.not.Throw();
});
}
function fail( verb, src, dst, opts, msg ) {
msg = msg || '.';
it( 'The ' + verb.toUpperCase() + ' command should FAIL' + msg, function () {
function runIt() {
FCMD.verbs[verb]( src, dst, opts, logMsg );
}
runIt.should.Throw();
});
}
});

View File

@ -7,6 +7,7 @@ var chai = require('chai')
, FRESHResume = require('../src/core/fresh-resume')
, CONVERTER = require('../src/core/convert')
, FS = require('fs')
, MKDIRP = require('mkdirp')
, _ = require('underscore');
chai.config.includeStack = false;
@ -17,10 +18,11 @@ describe('FRESH/JRS converter', function () {
it('should round-trip from JRS to FRESH to JRS without modifying or losing data', function () {
var fileA = path.join( __dirname, 'jrs-exemplar/richard-hendriks.json' );
var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' );
var fileA = path.join( __dirname, 'resumes/jrs-0.0.0/richard-hendriks.json' );
var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' );
_sheet = new FRESHResume().open( fileA );
MKDIRP.sync( path.parse(fileB).dir );
_sheet.saveAs( fileB, 'JRS' );
var rawA = FS.readFileSync( fileA, 'utf8' );

View File

@ -16,7 +16,7 @@ describe('jane-doe.json (FRESH)', function () {
it('should open without throwing an exception', function () {
function tryOpen() {
_sheet = new FRESHResume().open(
'node_modules/FRESCA/exemplar/jane-doe.json' );
'node_modules/jane-q-fullstacker/resume/jane-resume.json' );
}
tryOpen.should.not.Throw();
});
@ -43,19 +43,19 @@ describe('jane-doe.json (FRESH)', function () {
it('should save without throwing an exception', function(){
function trySave() {
_sheet.save( 'tests/sandbox/jane-doe.json' );
_sheet.save( 'tests/sandbox/jane-q-fullstacker.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new FRESHResume().open('tests/sandbox/jane-doe.json');
var savedSheet = new FRESHResume().open('tests/sandbox/jane-q-fullstacker.json');
_sheet.stringify().should.equal( savedSheet.stringify() )
});
it('should validate against the FRESH resume schema', function() {
var result = _sheet.isValid();
// var schemaJson = require('FRESCA');
// var schemaJson = require('fresca');
// var validate = validator( schemaJson, { verbose: true } );
// var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
@ -65,9 +65,3 @@ describe('jane-doe.json (FRESH)', function () {
});
// describe('subtract', function () {
// it('should return -1 when passed the params (1, 2)', function () {
// expect(math.subtract(1, 2)).to.equal(-1);
// });
// });

View File

@ -9,59 +9,61 @@ var chai = require('chai')
chai.config.includeStack = false;
describe('fullstack.json (JRS)', function () {
function testResume( opts ) {
describe( opts.title + ' (JRS)', function() {
opts.isValid = opts.isValid !== false;
var _sheet;
it('should open without throwing an exception', function () {
it('should open without throwing an exception', function () {
var that = this;
function tryOpen() {
_sheet = new JRSResume().open( 'node_modules/resample/fullstack/in/resume.json' );
_sheet = new JRSResume().open(
path.join( __dirname, 'resumes/jrs-0.0.0/' + opts.title + '.json' ) );
}
tryOpen.should.not.Throw();
});
it('should have one or more of each section', function() {
expect(
(_sheet.basics) &&
(_sheet.work && _sheet.work.length > 0) &&
(_sheet.skills && _sheet.skills.length > 0) &&
(_sheet.education && _sheet.education.length > 0) &&
(_sheet.volunteer && _sheet.volunteer.length > 0) &&
(_sheet.publications && _sheet.publications.length > 0) &&
(_sheet.awards && _sheet.awards.length > 0)
).to.equal( true );
var newObj = _.pick( _sheet, opts.sections );
expect( Object.keys(newObj).length ).to.equal( opts.sections.length );
});
it('should have a work duration of 11 years', function() {
_sheet.computed.numYears.should.equal( 11 );
it('should have a work duration of ' + opts.duration + ' years', function() {
_sheet.basics.computed.numYears.should.equal( opts.duration );
});
it('should save without throwing an exception', function(){
it('should save without throwing an exception', function() {
var that = this;
function trySave() {
_sheet.save( 'tests/sandbox/fullstack.json' );
_sheet.save( 'tests/sandbox/' + opts.title + '.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new JRSResume().open( 'tests/sandbox/fullstack.json' );
var savedSheet = new JRSResume().open( 'tests/sandbox/' + opts.title + '.json' );
_sheet.stringify().should.equal( savedSheet.stringify() )
});
it('should validate against the JSON Resume schema', function() {
var schemaJson = require('../src/core/resume.json');
var validate = validator( schemaJson, { verbose: true } );
var result = validate( JSON.parse( _sheet.imp.raw ) );
it('should ' + (opts.isValid ? '' : 'NOT ') + 'validate against the JSON Resume schema', function() {
var result = _sheet.isValid();
// var schemaJson = require('../src/core/resume.json');
// var validate = validator( schemaJson, { verbose: true } );
// var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", validate.errors, "\n\n");
result.should.equal( true );
"Validation errors:\n\n", _sheet.basics.imp.validationErrors, "\n\n");
result.should.equal( opts.isValid );
});
});
}
});
var sects = [ 'basics', 'work', 'volunteer', 'skills', 'education', 'publications', 'awards', 'references' ];
// describe('subtract', function () {
// it('should return -1 when passed the params (1, 2)', function () {
// expect(math.subtract(1, 2)).to.equal(-1);
// });
// });
testResume({ title: 'jane-q-fullstacker', duration: 7, sections: sects });
testResume({ title: 'jane-incomplete', duration: 0, sections: _.without(sects, 'awards', 'work') });
testResume({ title: 'richard-hendriks', duration: 1, sections: sects });
testResume({ title: 'empty', duration: 0, sections: [], isValid: false });

View File

@ -5,7 +5,7 @@ var chai = require('chai')
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, FCMD = require( '../src/fluentcmd')
, FCMD = require( '../src/hackmycmd')
, validator = require('is-my-json-valid')
, COLORS = require('colors');
@ -26,85 +26,28 @@ describe('Testing themes', function () {
useful: 'green',
});
it('HELLO-WORLD theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/hello-world/resume.all'];
var opts = {
theme: 'hello-world',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
function genTheme( themeName ) {
it( themeName.toUpperCase() + ' theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json'];
var dst = ['tests/sandbox/' + themeName + '/resume.all'];
var opts = {
theme: themeName,
format: 'FRESH',
prettify: true,
silent: true
};
FCMD.verbs.build( src, dst, opts, function() { } );
}
tryOpen.should.not.Throw();
});
}
it('COMPACT theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/compact/resume.all'];
var opts = {
theme: 'compact',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
it('MODERN theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/modern/resume.all'];
var opts = {
theme: 'modern',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
it('MINIMIST theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/minimist/resume.all'];
var opts = {
theme: 'minimist',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
it('AWESOME theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/awesome/resume.all'];
var opts = {
theme: 'awesome',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
genTheme('hello-world');
genTheme('compact');
genTheme('modern');
genTheme('minimist');
genTheme('awesome');
genTheme('positive');
});
// describe('subtract', function () {
// it('should return -1 when passed the params (1, 2)', function () {
// expect(math.subtract(1, 2)).to.equal(-1);
// });
// });