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

Compare commits

...

42 Commits

Author SHA1 Message Date
JD
090b8a271a Merge pull request #12 from fluentdesk/feat/rename
Rename "FluentCMD" to "FluentCV".
2015-10-27 03:51:22 -04:00
330866a518 Rename "FluentCMD" to "FluentCV". 2015-10-27 03:54:50 -04:00
JD
783525c21d Merge pull request #9 from fluentdesk/rfact/fluent-themes
Rename watermark to fluent-themes.
2015-10-26 13:45:39 -04:00
6d6f66bfe2 Rename watermark to fluent-themes. 2015-10-26 13:48:00 -04:00
JD
6f578f9d44 Merge pull request #8 from fluentdesk/fix/title
Always display title on error.
2015-10-26 13:14:32 -04:00
e34d02facb Always display title on error. 2015-10-26 13:17:58 -04:00
JD
0d0b8a9d0b Merge pull request #7 from fluentdesk/rfact/libmerge
rfact/libmerge
2015-10-26 12:56:11 -04:00
5f50485968 Expose API surface. 2015-10-26 12:54:27 -04:00
2fdb1ac36c Tweak metadata. 2015-10-26 12:54:15 -04:00
0aaa9ffff8 Introduce FluentLib sources. 2015-10-26 12:30:00 -04:00
21d9db2d99 Update README. 2015-10-26 11:27:13 -04:00
aacd50cebe Update README. 2015-10-26 10:18:33 -04:00
JD
ad75cdf02a Merge pull request #6 from fluentdesk/feat/partial-formats
Feat/partial formats
2015-10-26 10:06:52 -04:00
67e4e87275 Bump Watermark version. 2015-10-26 09:17:40 -04:00
4a98e0bb25 Multiple things.
1. Load themes directly in FCMD instead of only through FluentLib.
2. Add support for silent mode (`-s` or `--silent`).
3. Silently create output folder if not present (mkdirp).
2015-10-26 08:01:01 -04:00
06294a90b5 Add YAML output format support. 2015-10-26 02:45:37 -04:00
f7c05f49b2 Merge remote-tracking branch 'refs/remotes/origin/dev' 2015-10-25 08:19:21 -04:00
e20007721e Update README for 0.4.0. 2015-10-25 08:18:49 -04:00
19b6627df7 Bump version to 0.4.0. 2015-10-25 08:03:46 -04:00
68f943745c Support "--no-prettify" option.
Add command-line support for "--no-prettify" and "-n". Use a negative
option here since pretty printing is enabled by default.
2015-10-25 03:04:51 -04:00
2a7963450e Bump fluentlib version. 2015-10-25 02:16:54 -04:00
ceaef3e1c4 Bump version to 0.4.1. 2015-10-17 11:11:10 -04:00
1b4056371b Objectify options.
Support FCV Desktop requirement.
2015-10-17 11:10:59 -04:00
9dd759ffa2 Bump version to 0.3.1. 2015-10-10 23:27:20 -04:00
befa26cbb2 Merge remote-tracking branch 'refs/remotes/origin/dev' 2015-10-10 23:21:45 -04:00
31e2e12769 Bump fluentlib version. 2015-10-10 23:21:28 -04:00
f728004c4c Update README. 2015-10-10 22:36:13 -04:00
61386308a0 Merge remote-tracking branch 'refs/remotes/origin/dev' 2015-10-10 22:03:07 -04:00
2e5a68d942 Support explicit -o output parameter.
Allow output files to be explicitly specified via -o, for ex:

fluentcmd resume.json -o out/resume.html -o out/resume.pdf
2015-10-10 21:39:32 -04:00
cf15738f58 Fix explicit output file bug.
Fixes a bug that occurs when an explicit output file with an extension
other than .all is specified on the command line.
2015-10-10 21:38:06 -04:00
d4a3a08aff Add JSON format support. 2015-10-10 20:11:26 -04:00
b1d987c3b4 Bump version. 2015-10-10 20:09:09 -04:00
a2b482f60b Update README. 2015-10-10 18:34:36 -04:00
189f37b6a1 Add Markdown format support. 2015-10-10 17:49:29 -04:00
6e4263e58c Change default theme to "modern". 2015-10-10 15:39:13 -04:00
293f7e118a Merge branch 'dev' 2015-10-09 06:02:20 -04:00
961152850d Merge branch 'master' into dev 2015-10-09 06:00:27 -04:00
0724ffe7f2 Bump fluentlib version. 2015-10-09 05:56:36 -04:00
782068463c Merge branch 'dev' 2015-10-09 05:11:22 -04:00
f4dd72683b Enforce Unix line endings on JS files.
Sidestep finnicky line-ending behaviors on OS X and NPM registry.
2015-10-09 05:05:17 -04:00
9de7601a2a Peg fluentlib version and bump. 2015-10-07 09:51:08 -04:00
7ab2df6696 Improve error handling. 2015-10-07 09:29:41 -04:00
29 changed files with 1786 additions and 83 deletions

5
.gitattributes vendored
View File

@ -1,8 +1,7 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
*.js text eol=lf
*.json text eol=lf
# Standard to msysgit
*.doc diff=astextplain

1
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules/
tests/sandbox/

29
Gruntfile.js Normal file
View File

@ -0,0 +1,29 @@
'use strict';
module.exports = function (grunt) {
var opts = {
pkg: grunt.file.readJSON('package.json'),
simplemocha: {
options: {
globals: ['expect', 'should'],
timeout: 3000,
ignoreLeaks: false,
ui: 'bdd',
reporter: 'spec'
},
all: { src: ['tests/*.js'] }
}
};
grunt.initConfig( opts );
grunt.loadNpmTasks('grunt-simple-mocha');
grunt.registerTask('test', 'Test the FluentLib library.', function( config ) {
grunt.task.run( ['simplemocha:all'] );
});
grunt.registerTask('default', [ 'test' ]);
};

View File

@ -1,10 +1,10 @@
fluentcmd
=========
fluentCV
========
*Generate beautiful, targeted resumes from your command line or shell.*
FluentCMD is a **hackable, data-driven, dev-friendly resume authoring tool** with support for HTML, Markdown, Word, PDF, plain text, smoke signal, carrier pigeon, and other arbitrary-format resumes and CVs.
FluentCV is a **hackable, data-driven, dev-friendly resume authoring tool** with support for HTML, Markdown, Word, PDF, plain text, smoke signal, carrier pigeon, and other arbitrary-format resumes and CVs.
[![](assets/office_space.jpg)][8]
![](assets/fluentcv_cli_ubuntu.png)
Looking for a desktop GUI version with pretty timelines and graphs? Check out [FluentCV Desktop][7].
@ -13,7 +13,7 @@ Looking for a desktop GUI version with pretty timelines and graphs? Check out [F
- Runs on OS X, Linux, and Windows.
- Store your resume data as a durable, versionable JSON, YML, or XML document.
- Generate multiple targeted resumes in multiple formats, based on your needs.
- Output to HTML, PDF, Markdown, Word, JSON, XML, or other arbitrary formats.
- Output to HTML, PDF, Markdown, Word, JSON, YAML, XML, or a custom format.
- Never update one piece of information in four different resumes again.
- Compatible with the [JSON Resume standard][6] and [authoring tools][7].
- Free and open-source through the MIT license.
@ -22,57 +22,80 @@ Looking for a desktop GUI version with pretty timelines and graphs? Check out [F
## Install
FluentCMD requires a recent version of [Node.js][4] and [NPM][5]. Then:
FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then:
1. (Optional, for PDF support) Install the latest official [wkhtmltopdf][3] binary for your platform.
2. Install **fluentcmd** by running `npm install fluentcmd -g`.
3. You're ready to go!
2. Install **fluentCV** by running `npm install fluentcv -g`.
3. You're ready to go.
## Use
Assuming you've got a JSON-formatted resume handy, generating resumes in different formats and combinations easy. Just run:
`fluentcmd [inputs] [outputs] [-t theme]`.
```bash
fluentcv [inputs] [outputs] [-t theme].
```
Where `[inputs]` is one or more .json resume files, `[outputs]` is one or more destination resumes, and `[theme]` is the desired theme. For example:
Where `[inputs]` is one or more .json resume files, separated by spaces; `[outputs]` is one or more destination resumes, each prefaced with the `-o` option; and `[theme]` is the desired theme. For example:
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT)
fluentcmd resume.json resume.all -t informatic
# Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.)
fluentcv resume.json -o out/resume.all -t modern
# Generate a specific resume format
fluentcmd resume.json resume.html -t informatic
fluentcmd resume.json resume.txt -t informatic
fluentcmd resume.json resume.pdf -t informatic
fluentcmd resume.json resume.doc -t informatic
fluentcv resume.json -o out/resume.html
fluentcv resume.json -o out/resume.pdf
fluentcv resume.json -o out/resume.md
fluentcv resume.json -o out/resume.doc
fluentcv resume.json -o out/resume.json
fluentcv resume.json -o out/resume.txt
fluentcv resume.json -o out/resume.yml
# Specify 2 inputs and 3 outputs
fluentcv in1.json in2.json -o out.html -o out.doc -o out.pdf
```
You should see something to the effect of:
```
*** FluentCMD v0.1.0 ***
*** FluentCV v0.7.0 ***
Reading JSON resume: foo/resume.json
Applying MODERN Theme (7 formats)
Generating HTML resume: out/resume.html
Generating TXT resume: out/resume.txt
Generating DOC resume: out/resume.doc
Generating PDF resume: out/resume.pdf
Generating JSON resume: out/resume.json
Generating MARKDOWN resume: out/resume.md
Generating YAML resume: out/resume.yml
```
## Advanced
### 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.
```bash
fluentcv resume.json -t modern
fluentcv resume.json -t ~/foo/bar/my-custom-theme/
```
As of v0.7.0, available predefined themes are `modern`, `minimist`, and `hello-world`.
### Merging resumes
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
fluentcmd base.json specific.json resume.all
fluentcv base.json specific.json -o resume.all
```
This can be useful for overriding a base (generic) resume with information from a specific (targeted) resume. For example, you might override your generic catch-all "software developer" resume with specific details from your targeted "game developer" resume, or combine two partial resumes into a "complete" resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge:
```bash
fluentcmd in1.json in2.json in3.json in4.json out.html out.doc
fluentcv in1.json in2.json in3.json in4.json -o out.html -o out.doc
Reading JSON resume: in1.json
Reading JSON resume: in2.json
Reading JSON resume: in3.json
@ -84,34 +107,46 @@ Generating WORD resume: out.doc
### Multiple targets
You can specify **multiple output targets** and FluentCMD will build them:
You can specify **multiple output targets** and FluentCV will build them:
```bash
# Generate out1.doc, out1.pdf, and foo.txt from me.json.
fluentcmd me.json out1.doc out1.pdf foo.txt
fluentcv 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 "fluentcmd resume.json resume.all -t informatic"
fluentcmd resume.json
# Equivalent to "fluentcv resume.json resume.all -t modern"
fluentcv resume.json
```
### Using .all
The special `.all` extension tells FluentCMD to generate all supported output formats for the given resume. For example, this...
The special `.all` extension tells FluentCV to generate all supported output formats for the given resume. For example, this...
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, etc.)
fluentcmd input.json output.all
fluentcv me.json -o out/resume.all
```
..tells FluentCV to read `input.json` and generate `output.doc`, `output.html`, `output.txt`, `output.pdf`. That's more or less equivalent to:
..tells FluentCV to read `me.json` and generate `out/resume.md`, `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`.
### Prettifying
FluentCV applies [js-beautify][10]-style HTML prettification by default to HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag can be used:
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT)
fluentcmd input.json output.doc output.html output.txt output.pdf
fluentcv resume.json out.all --nopretty
```
### Silent Mode
Use `-s` or `--silent` to run in silent mode:
```bash
fluentcv resume.json -o someFile.all -s
fluentcv resume.json -o someFile.all --silent
```
## License
@ -127,3 +162,4 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
[7]: http://fluentcv.com
[8]: https://youtu.be/N9wsjroVlu8
[9]: https://api.jquery.com/jquery.extend/
[10]: https://github.com/beautify-web/js-beautify

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -1,10 +1,10 @@
{
"name": "fluentcmd",
"version": "0.1.0",
"description": "Generate beautiful, targeted resumes from your command line or shell.",
"name": "fluentcv",
"version": "0.7.0",
"description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.",
"repository": {
"type": "git",
"url": "https://github.com/fluentdesk/fluentcmd.git"
"url": "https://github.com/fluentdesk/fluentcv.git"
},
"keywords": [
"resume",
@ -16,16 +16,34 @@
"license": "MIT",
"preferGlobal": "true",
"bugs": {
"url": "https://github.com/fluentdesk/fluentcmd/issues"
"url": "https://github.com/fluentdesk/fluentcv/issues"
},
"main": "src/fluentcmd.js",
"bin": {
"fluentcmd": "src/index.js"
"fluentcv": "src/index.js"
},
"homepage": "https://github.com/fluentdesk/fluentcmd",
"homepage": "https://github.com/fluentdesk/fluentcv",
"dependencies": {
"fluentlib": "fluentdesk/fluentlib",
"fluent-themes": "0.1.0-beta",
"fs-extra": "^0.24.0",
"html": "0.0.10",
"is-my-json-valid": "^2.12.2",
"jst": "0.0.13",
"marked": "^0.3.5",
"minimist": "^1.2.0",
"underscore": "^1.8.3"
"mkdirp": "^0.5.1",
"moment": "^2.10.6",
"underscore": "^1.8.3",
"wkhtmltopdf": "^0.1.5",
"xml-escape": "^1.0.0",
"yamljs": "^0.2.4"
},
"devDependencies": {
"chai": "*",
"grunt": "*",
"grunt-simple-mocha": "*",
"is-my-json-valid": "^2.12.2",
"mocha": "*",
"resample": "fluentdesk/resample"
}
}

77
src/core/empty.json Normal file
View File

@ -0,0 +1,77 @@
{
"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": [""]
}]
}

80
src/core/fluent-date.js Normal file
View File

@ -0,0 +1,80 @@
/**
The FluentCV date representation.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
var moment = require('moment');
/**
Create a FluentDate from a string or Moment date object. There are a few date
formats to be aware of here.
1. The words "Present" and "Now", referring to the current date
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")
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
to specify a date format...but for maximum parsing safety and to avoid Moment
deprecation warnings, it's recommended to either a) explicitly specify the date
format or b) use an ISO format. For clarity, we handle these cases explicitly.
@class FluentDate
*/
function FluentDate( dt ) {
this.rep = this.fmt( dt );
}
FluentDate/*.prototype*/.fmt = function( dt ) {
if( (typeof dt === 'string' || dt instanceof String) ) {
dt = dt.toLowerCase().trim();
if( /^(present|now)$/.test(dt) ) { // "Present", "Now"
return moment();
}
else if( /^\D+\s+\d{4}$/.test(dt) ) { // "Mar 2015"
var parts = dt.split(' ');
var month = (months[parts[0]] || abbr[parts[0]]);
var dt = parts[1] + '-' + (month < 10 ? '0' + month : month.toString());
return moment( dt, 'YYYY-MM' );
}
else if( /^\d{4}-\d{1,2}$/.test(dt) ) { // "2015-03", "1998-4"
return moment( dt, 'YYYY-MM' );
}
else if( /^\s\d{4}$/.test(dt) ) { // "2015"
return moment( dt, 'YYYY' );
}
else if( /^\s*$/.test(dt) ) { // "", " "
var defTime = {
isNull: true,
isBefore: function( other ) {
return( other && !other.isNull ) ? true : false;
},
isAfter: function( other ) {
return( other && !other.isNull ) ? false : false;
},
unix: function() { return 0; },
format: function() { return ''; },
diff: function() { return 0; }
};
return defTime;
}
else {
var mt = moment( dt );
if(mt.isValid())
return mt;
throw 'Invalid date format encountered.';
}
}
else {
if( dt.isValid && dt.isValid() )
return dt;
throw 'Unknown date object encountered.';
}
};
var months = {}, abbr = {};
moment.months().forEach(function(m,idx){months[m.toLowerCase()]=idx+1;});
moment.monthsShort().forEach(function(m,idx){abbr[m.toLowerCase()]=idx+1;});
abbr.sept = 9;
module.exports = FluentDate;

380
src/core/resume.json Normal file
View File

@ -0,0 +1,380 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Resume Schema",
"type": "object",
"additionalProperties": false,
"properties": {
"basics": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string"
},
"label": {
"type": "string",
"description": "e.g. Web Developer"
},
"picture": {
"type": "string",
"description": "URL (as per RFC 3986) to a picture in JPEG or PNG format"
},
"email": {
"type": "string",
"description": "e.g. thomas@gmail.com",
"format": "email"
},
"phone": {
"type": "string",
"description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923"
},
"website": {
"type": "string",
"description": "URL (as per RFC 3986) to your website, e.g. personal homepage",
"format": "uri"
},
"summary": {
"type": "string",
"description": "Write a short 2-3 sentence biography about yourself"
},
"location": {
"type": "object",
"additionalProperties": true,
"properties": {
"address": {
"type": "string",
"description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li."
},
"postalCode": {
"type": "string"
},
"city": {
"type": "string"
},
"countryCode": {
"type": "string",
"description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN"
},
"region": {
"type": "string",
"description": "The general region where you live. Can be a US state, or a province, for instance."
}
}
},
"profiles": {
"type": "array",
"description": "Specify any number of social networks that you participate in",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"network": {
"type": "string",
"description": "e.g. Facebook or Twitter"
},
"username": {
"type": "string",
"description": "e.g. neutralthoughts"
},
"url": {
"type": "string",
"description": "e.g. http://twitter.com/neutralthoughts"
}
}
}
}
}
},
"work": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"company": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"volunteer": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"organization": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"education": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"institution": {
"type": "string",
"description": "e.g. Massachusetts Institute of Technology"
},
"area": {
"type": "string",
"description": "e.g. Arts"
},
"studyType": {
"type": "string",
"description": "e.g. Bachelor"
},
"startDate": {
"type": "string",
"description": "e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"gpa": {
"type": "string",
"description": "grade point average, e.g. 3.67/4.0"
},
"courses": {
"type": "array",
"description": "List notable courses/subjects",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. H1302 - Introduction to American history"
}
}
}
}
},
"awards": {
"type": "array",
"description": "Specify any awards you have received throughout your professional career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"title": {
"type": "string",
"description": "e.g. One of the 100 greatest minds of the century"
},
"date": {
"type": "string",
"description": "e.g. 1989-06-12",
"format": "date"
},
"awarder": {
"type": "string",
"description": "e.g. Time Magazine"
},
"summary": {
"type": "string",
"description": "e.g. Received for my work with Quantum Physics"
}
}
}
},
"publications": {
"type": "array",
"description": "Specify your publications through your career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. The World Wide Web"
},
"publisher": {
"type": "string",
"description": "e.g. IEEE, Computer Magazine"
},
"releaseDate": {
"type": "string",
"description": "e.g. 1990-08-01"
},
"website": {
"type": "string",
"description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html"
},
"summary": {
"type": "string",
"description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML."
}
}
}
},
"skills": {
"type": "array",
"description": "List out your professional skill-set",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Web Development"
},
"level": {
"type": "string",
"description": "e.g. Master"
},
"keywords": {
"type": "array",
"description": "List some keywords pertaining to this skill",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. HTML"
}
}
}
}
},
"languages": {
"type": "array",
"description": "List any other languages you speak",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"language": {
"type": "string",
"description": "e.g. English, Spanish"
},
"fluency": {
"type": "string",
"description": "e.g. Fluent, Beginner"
}
}
}
},
"interests": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Philosophy"
},
"keywords": {
"type": "array",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Friedrich Nietzsche"
}
}
}
}
},
"references": {
"type": "array",
"description": "List references you have received",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Timothy Cook"
},
"reference": {
"type": "string",
"description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing."
}
}
}
}
}
}

259
src/core/sheet.js Normal file
View File

@ -0,0 +1,259 @@
/**
Abstract character/resume sheet representation.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
(function() {
var FS = require('fs')
, extend = require('../utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, PATH = require('path')
, moment = require('moment');
/**
The Sheet class represent a specific JSON character sheet. When Sheet.open
is called, we merge the loaded JSON sheet properties onto the Sheet instance
via extend(), so a full-grown sheet object will have all of the methods here,
plus a complement of JSON properties from the backing JSON file. That allows
us to treat Sheet objects interchangeably with the loaded JSON model.
@class Sheet
*/
function Sheet() {
}
/**
Open and parse the specified JSON resume sheet. Merge the JSON object model
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
*/
Sheet.prototype.open = function( file, title ) {
this.meta = { fileName: file };
this.meta.raw = FS.readFileSync( file, 'utf8' );
return this.parse( this.meta.raw, title );
};
/**
Save the sheet to disk (for environments that have disk access).
*/
Sheet.prototype.save = function( filename ) {
filename = filename || this.meta.fileName;
FS.writeFileSync( filename, this.stringify(), 'utf8' );
return this;
};
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
Sheet.prototype.stringify = function() {
function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
return JSON.stringify( this, replacer, 2 );
};
/**
Open and parse the specified JSON resume sheet. Merge the JSON object model
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
*/
Sheet.prototype.parse = function( stringData, opts ) {
opts = opts || { };
var rep = JSON.parse( stringData );
extend( true, this, rep );
// Set up metadata
if( opts.meta === undefined || opts.meta ) {
this.meta = this.meta || { };
this.meta.title = (opts.title || this.meta.title) || this.basics.name;
}
// Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this );
(opts.sort === undefined || opts.sort) && this.sort();
(opts.compute === undefined || opts.compute) && (this.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
return this;
};
/**
Return a unique list of all keywords across all skills.
*/
Sheet.prototype.keywords = function() {
var flatSkills = [];
if( this.skills && this.skills.length ) {
this.skills.forEach( function( s ) {
flatSkills = _.union( flatSkills, s.keywords );
});
}
return flatSkills;
},
/**
Update the sheet's raw data. TODO: remove/refactor
*/
Sheet.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str )
return this;
};
/**
Reset the sheet to an empty state.
*/
Sheet.prototype.clear = function( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
clearMeta && (delete this.meta);
delete this.computed; // Don't use Object.keys() here
delete this.work;
delete this.volunteer;
delete this.education;
delete this.awards;
delete this.publications;
delete this.interests;
delete this.skills;
};
/**
Get the default (empty) sheet.
*/
Sheet.default = function() {
return new Sheet().open( PATH.join( __dirname, 'empty.json'), 'Empty' );
}
/**
Add work experience to the sheet.
*/
Sheet.prototype.add = function( moniker ) {
var defSheet = Sheet.default();
var newObject = $.extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject );
return newObject;
};
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
Sheet.prototype.hasProfile = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.basics.profiles && _.some( this.basics.profiles, function(p) {
return p.network.trim().toLowerCase() === socialNetwork;
});
};
/**
Determine if the sheet includes a specific skill.
*/
Sheet.prototype.hasSkill = function( skill ) {
skill = skill.trim().toLowerCase();
return this.skills && _.some( this.skills, function(sk) {
return sk.keywords && _.some( sk.keywords, function(kw) {
return kw.trim().toLowerCase() === skill;
});
});
};
/**
Validate the sheet against the JSON Resume schema.
*/
Sheet.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓
var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' );
var schemaObj = JSON.parse( schema );
var validator = require('is-my-json-valid')
var validate = validator( schemaObj );
return validate( this );
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
Sheet.prototype.duration = function() {
if( this.work && this.work.length ) {
var careerStart = this.work[ this.work.length - 1].safeStartDate;
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
!careerStart.trim())
return 0;
var careerLast = _.max( this.work, function( w ) {
return w.safeEndDate.unix();
}).safeEndDate;
return careerLast.diff( careerStart, 'years' );
}
return 0;
};
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
Sheet.prototype.sort = function( ) {
this.work && this.work.sort( byDateDesc );
this.education && this.education.sort( byDateDesc );
this.volunteer && this.volunteer.sort( byDateDesc );
this.awards && this.awards.sort( function(a, b) {
return( a.safeDate.isBefore(b.safeDate) ) ? 1
: ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
});
this.publications && this.publications.sort( function(a, b) {
return( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) ? 1
: ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0;
});
function byDateDesc(a,b) {
return( a.safeStartDate.isBefore(b.safeStartDate) ) ? 1
: ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0;
}
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
function _parseDates() {
var _fmt = require('./fluent-date').fmt;
this.work && this.work.forEach( function(job) {
job.safeStartDate = _fmt( job.startDate );
job.safeEndDate = _fmt( job.endDate );
});
this.education && this.education.forEach( function(edu) {
edu.safeStartDate = _fmt( edu.startDate );
edu.safeEndDate = _fmt( edu.endDate );
});
this.volunteer && this.volunteer.forEach( function(vol) {
vol.safeStartDate = _fmt( vol.startDate );
vol.safeEndDate = _fmt( vol.endDate );
});
this.awards && this.awards.forEach( function(awd) {
awd.safeDate = _fmt( awd.date );
});
this.publications && this.publications.forEach( function(pub) {
pub.safeReleaseDate = _fmt( pub.releaseDate );
});
}
/**
Export the Sheet function/ctor.
*/
module.exports = Sheet;
}());

90
src/core/theme.js Normal file
View File

@ -0,0 +1,90 @@
/**
Abstract theme representation.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
*/
(function() {
var FS = require('fs')
, extend = require('../utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, PATH = require('path')
, moment = require('moment');
/**
The Theme class represents a specific presentation of a resume.
@class Theme
*/
function Theme() {
}
/**
Open and parse the specified theme.
*/
Theme.prototype.open = function( themeFolder ) {
function friendlyName( val ) {
val = val.trim().toLowerCase();
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val;
}
var tplFolder = PATH.join( themeFolder, 'templates' );
var fmts = FS.readdirSync( tplFolder ).map( function( file ) {
var absPath = PATH.join( tplFolder, file );
var pathInfo = PATH.parse(absPath);
var temp = [ pathInfo.name, {
title: friendlyName(pathInfo.name),
pre: pathInfo.name,
ext: pathInfo.ext.slice(1),
path: absPath,
data: FS.readFileSync( absPath, 'utf8' ),
css: null
}];
return temp;
});
// Freebie formats every theme gets
fmts.push( [ 'json', { title: 'json', pre: 'json', ext: 'json', path: null, data: null } ] );
fmts.push( [ 'yml', { title: 'yaml', pre: 'yml', ext: 'yml', path: null, data: null } ] );
// Handle CSS files
var cssFiles = fmts.filter(function( fmt ){
return fmt[1].ext === 'css';
});
cssFiles.forEach(function( cssf ) {
// For each CSS file, get its corresponding HTML file
var idx = _.findIndex(fmts, function( fmt ) { return fmt[1].pre === cssf[1].pre && fmt[1].ext === 'html' });
fmts[ idx ][1].css = cssf[1].data;
fmts[ idx ][1].cssPath = cssf[1].path;
});
fmts = fmts.filter( function( fmt) {
return fmt[1].ext !== 'css';
});
// Create a hash out of the formats for this theme
this.formats = _.object( fmts );
this.name = PATH.parse( themeFolder ).name;
return this;
};
/**
Determine if the theme supports the specified output format.
*/
Theme.prototype.hasFormat = function( fmt ) {
return _.has( this.formats, fmt );
};
/**
Determine if the theme supports the specified output format.
*/
Theme.prototype.getFormat = function( fmt ) {
return this.formats[ fmt ];
};
module.exports = Theme;
}());

View File

@ -1,5 +1,5 @@
/**
Internal resume generation logic for FluentCMD.
Internal resume generation logic for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
*/
@ -9,31 +9,39 @@ module.exports = function () {
var path = require( 'path' )
, extend = require( './utils/extend' )
, unused = require('./utils/string')
, FLUENT = require('fluentlib')
, fs = require('fs')
, _ = require('underscore')
, rez, _log;
, FLUENT = require('./fluentlib')
, PATH = require('path')
, MKDIRP = require('mkdirp')
, 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 "informatic".
@param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
*/
function gen( src, dst, theme, logger ) {
function gen( src, dst, opts, logger, errHandler ) {
_log = logger || console.log;
_opts.theme = theme;
var msg = '';
_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 = src.map( function( res ) {
_log( 'Reading JSON resume: ' + res );
return (new FLUENT.Sheet()).open( res );
});
// Merge input resumes...
var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ? 'Merging ' + a.meta.fileName : '')
+ ' onto ' + b.meta.fileName;
@ -41,16 +49,32 @@ module.exports = function () {
});
msg && _log(msg);
// Load the active theme
// Verify the specified theme name/path
var tFolder = PATH.resolve( __dirname, '../node_modules/fluent-themes/themes', _opts.theme );
var exists = require('./utils/file-exists');
if (!exists( tFolder )) {
tFolder = PATH.resolve( _opts.theme );
if (!exists( tFolder )) {
throw { fluenterror: 1, data: _opts.theme };
}
}
var theTheme = new FLUENT.Theme().open( tFolder );
_opts.themeObj = theTheme;
_log( 'Applying ' + theTheme.name.toUpperCase() + ' theme (' + Object.keys(theTheme.formats).length + ' formats)' );
// Expand output resumes... (can't use map() here)
var targets = [];
var 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' ?
_fmts.map(function(z){ return to.replace(/all$/g,z.name); }) : [to]);
Object.keys( theTheme.formats ).map(function(k){ var z = theTheme.formats[k]; return { file: to.replace(/all$/g,z.pre), fmt: z } })
: [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
});
// Run the transformation!
var finished = targets.map( single );
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 };
@ -61,34 +85,28 @@ module.exports = function () {
@param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function single( f ) {
function single( fi, theme ) {
try {
// Get the output file type (pdf, html, txt, etc)
var fType = path.extname( f ).trim().toLowerCase().substr(1);
var fName = path.basename( f, '.' + fType );
// Get the format object (if any) corresponding to that type, and assemble
// the final output file path for the generated resume.
var fObj = _fmts.filter( function(_f) { return _f.name === fType; } )[0];
var fOut = path.join( f.substring( 0, f.lastIndexOf('.') + 1 ) + fObj.ext );
// Generate!
_log( 'Generating ' + fType.toUpperCase() + ' resume: ' + path.relative(process.cwd(), f) );
return fObj.gen.generate( rez, fOut, _opts.theme );
var f = fi.file, fType = fi.fmt.ext, fName = path.basename( f, '.' + fType );
var fObj = _.property( fi.fmt.pre )( theme.formats );
var fOut = path.join( f.substring( 0, f.lastIndexOf('.') + 1 ) + fObj.pre );
_log( 'Generating ' + fi.fmt.title.toUpperCase() + ' resume: ' + path.relative(process.cwd(), f ) );
var theFormat = _fmts.filter( function( fmt ) {
return fmt.name === fi.fmt.pre;
})[0];
MKDIRP( path.dirname(fOut) ); // Ensure dest folder exists; don't bug user
theFormat.gen.generate( rez, fOut, _opts );
}
catch( ex ) {
err( ex );
_err( ex );
}
}
/**
Handle an exception.
*/
function err( ex ) {
var msg = ex.toString();
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
_log( 'ERROR: ' + trimmed.toString() );
function error( ex ) {
throw ex;
}
/**
@ -98,21 +116,31 @@ module.exports = function () {
{ 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: '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() }
];
/**
Default options.
Default FluentCMD options.
*/
var _opts = {
theme: 'informatic',
}
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 {
generate: gen,
lib: require('./fluentlib'),
options: _opts,
formats: _fmts
};

17
src/fluentlib.js Normal file
View File

@ -0,0 +1,17 @@
/**
Core resume generation module for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
module.exports = {
Sheet: require('./core/sheet'),
Theme: require('./core/theme'),
HtmlGenerator: require('./gen/html-generator'),
TextGenerator: require('./gen/text-generator'),
HtmlPdfGenerator: require('./gen/html-pdf-generator'),
WordGenerator: require('./gen/word-generator'),
MarkdownGenerator: require('./gen/markdown-generator'),
JsonGenerator: require('./gen/json-generator'),
YamlGenerator: require('./gen/yaml-generator'),
JsonYamlGenerator: require('./gen/json-yaml-generator')
};

43
src/gen/base-generator.js Normal file
View File

@ -0,0 +1,43 @@
/**
Base resume generator for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
*/
(function() {
// Use J. Resig's nifty class implementation
var Class = require( '../utils/class' );
/**
The BaseGenerator class is the root of the generator hierarchy. Functionality
common to ALL generators lives here.
*/
var BaseGenerator = module.exports = Class.extend({
/**
Base-class initialize.
*/
init: function( outputFormat ) {
this.format = outputFormat;
},
/**
Status codes.
*/
codes: {
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3
},
/**
Generator options.
*/
opts: {
}
});
}());

32
src/gen/html-generator.js Normal file
View File

@ -0,0 +1,32 @@
/**
HTML resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
var TemplateGenerator = require('./template-generator');
var FS = require('fs-extra');
var HTML = require( 'html' );
var HtmlGenerator = module.exports = TemplateGenerator.extend({
init: function() {
this._super( 'html' );
},
/**
Generate an HTML resume with optional pretty printing.
*/
onBeforeSave: function( mk, theme, outputFile ) {
var themeFile = theme.getFormat('html').path;
var cssSrc = themeFile.replace( /.html$/g, '.css' );
var cssDst = outputFile.replace( /.html$/g, '.css' );
var that = this;
FS.copySync( cssSrc, cssDst, { clobber: true }, function( e ) {
throw { fluenterror: that.codes.copyCss, data: [cssSrc,cssDst] };
});
return this.opts.prettify ?
HTML.prettyPrint( mk, this.opts.prettify ) : mk;
}
});

View File

@ -0,0 +1,74 @@
/**
HTML-based PDF resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
var TemplateGenerator = require('./template-generator');
var FS = require('fs-extra');
var HTML = require( 'html' );
var HtmlPdfGenerator = module.exports = TemplateGenerator.extend({
init: function() {
this._super( 'pdf', 'html' );
},
/**
Generate an HTML resume with optional pretty printing.
TODO: Avoid copying the CSS file to dest if we don't need to.
*/
onBeforeSave: function( mk, themeFile, outputFile ) {
// var cssSrc = themeFile.replace( /pdf\.html$/gi, 'html.css' );
// var cssDst = outputFile.replace( /\.pdf$/gi, '.css' );
// var that = this;
// FS.copySync( cssSrc, cssDst, { clobber: true }, function( e ) {
// if( e ) that.err( "Couldn't copy CSS file to destination: " + e);
// });
// return true ?
// HTML.prettyPrint( mk, { indent_size: 2 } ) : mk;
pdf(mk, outputFile);
return mk;
}
});
/**
Generate a PDF from HTML.
*/
function pdf( markup, fOut ) {
var pdfCount = 0;
if( false ) { //( _opts.pdf === 'phantom' || _opts.pdf == 'all' ) {
pdfCount++;
require('phantom').create( function( ph ) {
ph.createPage( function( page ) {
page.setContent( markup );
page.set('paperSize', {
format: 'A4',
orientation: 'portrait',
margin: '1cm'
});
page.set("viewportSize", {
width: 1024, // TODO: option-ify
height: 768 // TODO: Use "A" sizes
});
page.set('onLoadFinished', function(success) {
page.render( fOut );
pdfCount++;
ph.exit();
});
},
{ dnodeOpts: { weak: false } } );
});
}
if( true ) { // _opts.pdf === 'wkhtmltopdf' || _opts.pdf == 'all' ) {
var fOut2 = fOut;
if( pdfCount == 1 ) {
fOut2 = fOut2.replace(/\.pdf$/g, '.b.pdf');
}
require('wkhtmltopdf')( markup, { pageSize: 'letter' } )
.pipe( FS.createWriteStream( fOut2 ) );
pdfCount++;
}
}

35
src/gen/json-generator.js Normal file
View File

@ -0,0 +1,35 @@
/**
Definition of the JsonGenerator class.
@license Copyright (c) 2015 | James M. Devlin
*/
var BaseGenerator = require('./base-generator');
var FS = require('fs');
var _ = require('underscore');
/**
The JsonGenerator generates a JSON resume directly.
*/
var JsonGenerator = module.exports = BaseGenerator.extend({
init: function(){
this._super( 'json' );
},
invoke: function( rez ) {
// TODO: merge with FCVD
function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
return JSON.stringify( rez, replacer, 2 );
},
generate: function( rez, f ) {
FS.writeFileSync( f, this.invoke(rez), 'utf8' );
}
});

View File

@ -0,0 +1,37 @@
/**
A JSON-driven YAML resume generator for FluentLib.
@module json-yaml-generator.js
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
*/
(function() {
var BaseGenerator = require('./base-generator');
var FS = require('fs');
var YAML = require('yamljs');
/**
JsonYamlGenerator takes a JSON resume object and translates it directly to
JSON without a template, producing an equivalent YAML-formatted resume. See
also YamlGenerator (yaml-generator.js).
*/
var JsonYamlGenerator = module.exports = BaseGenerator.extend({
init: function(){
this._super( 'yml' );
},
invoke: function( rez, themeMarkup, cssInfo, opts ) {
return YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 );
},
generate: function( rez, f, opts ) {
var data = YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 );
FS.writeFileSync( f, data, 'utf8' );
}
});
}());

View File

@ -0,0 +1,17 @@
/**
Markdown resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
var TemplateGenerator = require('./template-generator');
/**
MarkdownGenerator generates a Markdown-formatted resume via TemplateGenerator.
*/
var MarkdownGenerator = module.exports = TemplateGenerator.extend({
init: function(){
this._super( 'md', 'txt' );
}
});

View File

@ -0,0 +1,173 @@
/**
Template-based resume generator base for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
*/
var FS = require( 'fs' )
, _ = require( 'underscore' )
, MD = require( 'marked' )
, XML = require( 'xml-escape' )
, PATH = require('path')
, BaseGenerator = require( './base-generator' )
, EXTEND = require('../utils/extend')
, Theme = require('../core/theme');
// Default options.
var _defaultOpts = {
themeRelative: '../../node_modules/fluent-themes/themes',
keepBreaks: true,
freezeBreaks: true,
nSym: '&newl;', // newline entity
rSym: '&retn;', // return entity
template: {
interpolate: /\{\{(.+?)\}\}/g,
escape: /\{\{\=(.+?)\}\}/g,
evaluate: /\{\%(.+?)\%\}/g,
comment: /\{\#(.+?)\#\}/g
},
filters: {
out: function( txt ) { return txt; },
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, ''); },
lower: function( txt ) { return txt.toLowerCase(); }
},
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
}
};
/**
TemplateGenerator performs resume generation via Underscore-style template
expansion and is appropriate for text-based formats like HTML, plain text,
and XML versions of Microsoft Word, Excel, and OpenOffice.
*/
var TemplateGenerator = module.exports = BaseGenerator.extend({
/** outputFormat: html, txt, pdf, doc
templateFormat: html or txt
**/
init: function( outputFormat, templateFormat, cssFile ){
this._super( outputFormat );
this.tplFormat = templateFormat || outputFormat;
},
/** Default generation method for template-based generators. */
invoke: function( rez, themeMarkup, cssInfo, opts ) {
// Compile and invoke the template!
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. */
generate: function( rez, f, opts ) {
// Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts );
// Verify the specified theme name/path
var tFolder = PATH.resolve( __dirname, this.opts.themeRelative, this.opts.theme );
var exists = require('../utils/file-exists');
if (!exists( tFolder )) {
tFolder = PATH.resolve( this.opts.theme );
if (!exists( tFolder )) {
throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme };
}
}
// Load the theme
var theme = opts.themeObj || new Theme().open( tFolder );
// Load theme and CSS data
var tplFolder = PATH.join( tFolder, 'templates' );
var curFmt = theme.getFormat( this.format );
var ctx = { file: curFmt.css ? curFmt.cssPath : null, data: curFmt.css || null };
// Compile and invoke the template!
var mk = this.single( rez, curFmt.data, this.format, ctx, opts );
this.onBeforeSave && (mk = this.onBeforeSave( mk, theme, f ));
FS.writeFileSync( f, mk, { encoding: 'utf8', flags: 'w' } );
},
/**
Perform a single resume JSON-to-DEST resume transformation. Exists as a
separate function in order to expose string-based transformations to clients
who don't have access to filesystem resources (in-browser, etc.).
*/
single: function( json, jst, format, cssInfo, opts ) {
// Freeze whitespace in the template.
this.opts.freezeBreaks && ( jst = freeze(jst) );
// Tweak underscore's default template delimeters
_.templateSettings = this.opts.template;
// Convert {{ someVar }} to {% print(filt.out(someVar) %}
// Convert {{ someVar|someFilter }} to {% print(filt.someFilter(someVar) %}
jst = jst.replace( _.templateSettings.interpolate, function replace(m, p1) {
if( p1.indexOf('|') > -1 ) {
var terms = p1.split('|');
return '{% print( filt.' + terms[1] + '( ' + terms[0] + ' )) %}';
}
else {
return '{% print( filt.out(' + p1 + ') ) %}';
}
});
// Strip {# comments #}
jst = jst.replace( _.templateSettings.comment, '');
json.display_progress_bar = true;
// Compile and run the template. TODO: avoid unnecessary recompiles.
jst = _.template(jst)({ r: json, filt: this.opts.filters, cssInfo: cssInfo, headFragment: this.opts.headFragment || '' });
// Unfreeze whitespace
this.opts.freezeBreaks && ( jst = unfreeze(jst) );
return jst;
}
});
/**
Export the TemplateGenerator function/ctor.
*/
module.exports = TemplateGenerator;
/**
Freeze newlines for protection against errant JST parsers.
*/
function freeze( markup ) {
return markup
.replace( _reg.regN, _defaultOpts.nSym )
.replace( _reg.regR, _defaultOpts.rSym );
}
/**
Unfreeze newlines when the coast is clear.
*/
function unfreeze( markup ) {
return markup
.replace( _reg.regSymR, '\r' )
.replace( _reg.regSymN, '\n' );
}
/**
Regexes for linebreak preservation.
*/
var _reg = {
regN: new RegExp( '\n', 'g' ),
regR: new RegExp( '\r', 'g' ),
regSymN: new RegExp( _defaultOpts.nSym, 'g' ),
regSymR: new RegExp( _defaultOpts.rSym, 'g' )
};

19
src/gen/text-generator.js Normal file
View File

@ -0,0 +1,19 @@
/**
Plain text resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
var TemplateGenerator = require('./template-generator');
/**
The TextGenerator generates a plain-text resume via the TemplateGenerator.
*/
var TextGenerator = TemplateGenerator.extend({
init: function(){
this._super( 'txt' );
},
});
module.exports = TextGenerator;

13
src/gen/word-generator.js Normal file
View File

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

17
src/gen/xml-generator.js Normal file
View File

@ -0,0 +1,17 @@
/**
XML resume generator for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
*/
var BaseGenerator = require('./base-generator');
/**
The XmlGenerator generates an XML resume via the TemplateGenerator.
*/
var XmlGenerator = module.exports = BaseGenerator.extend({
init: function(){
this._super( 'xml' );
},
});

24
src/gen/yaml-generator.js Normal file
View File

@ -0,0 +1,24 @@
/**
A YAML resume generator for FluentLib.
@module yaml-generator.js
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
*/
(function() {
var TemplateGenerator = require('./template-generator');
/**
YamlGenerator generates a YAML-formatted resume via TemplateGenerator.
*/
var YamlGenerator = module.exports = TemplateGenerator.extend({
init: function(){
this._super( 'yml', 'yml' );
}
});
}());

View File

@ -1,25 +1,78 @@
#! /usr/bin/env node
/**
Command-line interface (CLI) for FluentCMD via Node.js.
Command-line interface (CLI) for FluentCV via Node.js.
@license Copyright (c) 2015 | James M. Devlin
*/
var ARGS = require( 'minimist' )
, FCMD = require( './fluentcmd');
, FCMD = require( './fluentcmd')
, PKG = require('../package.json')
, opts = { };
try {
console.log( '*** FluentCMD v0.1.0 ***' );
if( process.argv.length <= 2 ) { throw 'Please specify a JSON resume file.'; }
var args = ARGS( process.argv.slice(2) );
var src = args._.filter( function( a ) { return a.endsWith('.json'); });
var dst = args._.filter( function( a ) { return !a.endsWith('.json'); });
FCMD.generate( src, dst, args.t || 'informatic' );
process.platform !== 'win32' && console.log('\n');
main();
}
catch( ex ) {
var msg = ex.toString();
handleError( ex );
}
function main() {
// Setup.
var title = '*** FluentCV v' + PKG.version + ' ***';
if( process.argv.length <= 2 ) { logMsg(title); throw { fluenterror: 3 }; }
var args = ARGS( process.argv.slice(2) );
opts = getOpts( args );
logMsg( title );
// Convert arguments to source files, target files, options
var src = args._ || [];
var dst = (args.o && ((typeof args.o === 'string' && [ args.o ]) || args.o)) || [];
dst = (dst === true) ? [] : dst; // Handle -o with missing output file
// Generate!
FCMD.generate( src, dst, opts, logMsg );
process.exit(0);
}
function logMsg( msg ) {
opts.silent || console.log( msg );
}
function getOpts( args ) {
var noPretty = args['nopretty'] || args.n;
noPretty = noPretty && (noPretty === true || noPretty === 'true');
return {
theme: args.t || 'modern',
prettify: !noPretty,
silent: args.s || args.silent
};
}
function handleError( ex ) {
var msg = '', exitCode;
if( ex.fluenterror ){
switch( ex.fluenterror ) { // TODO: Remove magic numbers
case 1: msg = "The specified theme couldn't be found: " + ex.data; break;
case 2: msg = "Couldn't copy CSS file to destination folder"; break;
case 3: msg = "Please specify a valid JSON resume file."; break;
};
exitCode = ex.fluenterror;
}
else {
msg = ex.toString();
exitCode = 4;
}
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
console.log( 'ERROR: ' + trimmed.toString() );
process.exit( exitCode );
}

67
src/utils/class.js Normal file
View File

@ -0,0 +1,67 @@
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
* http://ejohn.org/blog/simple-javascript-inheritance/
*/
// Inspired by base2 and Prototype
(function(){
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function(){};
module.exports = Class;
// Create a new Class that inherits from this class
Class.extend = function(prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.prototype.constructor = Class;
// And make this class extendable
Class.extend = arguments.callee;
return Class;
};
})();

View File

@ -1,6 +1,6 @@
/**
Plain JavaScript replacement of jQuery .extend based on jQuery sources.
@license Copyright (c) 2015 | James M. Devlin
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
function _extend() {

18
src/utils/file-exists.js Normal file
View File

@ -0,0 +1,18 @@
/**
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');
}
};

67
tests/test-sheet.js Normal file
View File

@ -0,0 +1,67 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, Sheet = require('../src/core/sheet')
, validator = require('is-my-json-valid');
chai.config.includeStack = false;
describe('fullstack.json', function () {
var _sheet;
it('should open without throwing an exception', function () {
function tryOpen() {
_sheet = new Sheet().open( 'node_modules/resample/fullstack/in/resume.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 );
});
it('should have a work duration of 11 years', function() {
_sheet.computed.numYears.should.equal( 11 );
});
it('should save without throwing an exception', function(){
function trySave() {
_sheet.save( 'tests/sandbox/fullstack.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new Sheet().open( 'tests/sandbox/fullstack.json' );
_sheet.stringify().should.equal( savedSheet.stringify() )
});
it('should validate against the JSON Resume schema', function() {
var schemaJson = require('../src/core/resume.json');
var validate = validator( schemaJson, { verbose: true } );
var result = validate( JSON.parse( _sheet.meta.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", validate.errors, "\n\n");
result.should.equal( true );
});
});
// describe('subtract', function () {
// it('should return -1 when passed the params (1, 2)', function () {
// expect(math.subtract(1, 2)).to.equal(-1);
// });
// });