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

Compare commits

...

93 Commits

Author SHA1 Message Date
a3cefa1c42 Merge pull request #20 from fluentdesk/v0.10.0
v0.10.0
2015-12-09 23:28:25 -05:00
3805a36271 Fix folder generation wrinkle. 2015-12-09 23:30:53 -05:00
81276cf2cc Update README. 2015-12-09 23:09:33 -05:00
541198321e Fix JSHint warnings. 2015-12-09 21:44:35 -05:00
91aba39050 Add file LINTing through JSHint. 2015-12-09 21:44:18 -05:00
f7a3da0a4d Add generator tests for all themes. 2015-12-09 05:41:04 -05:00
0395792359 Restore canonical output filename. 2015-12-09 05:08:10 -05:00
2abfe4426c Refactor. 2015-12-09 04:32:48 -05:00
3dcf3c3974 Tweak Markdownification. 2015-12-09 04:32:39 -05:00
857de65750 More MEGADESK. 2015-12-09 00:13:58 -05:00
f3c9f92263 Add baseline Markdownification. 2015-12-08 22:22:33 -05:00
e8704e1374 Fix file generation glitch. 2015-12-08 22:22:14 -05:00
87c03b437c Generate safe date times; don't hard-code. 2015-12-08 22:21:42 -05:00
1a757e8a87 Bump FRESCA version to 0.2.0. 2015-12-08 21:12:19 -05:00
7c58f0ea96 Add symlink support. 2015-12-08 10:13:04 -05:00
fcaeb381fe Gather. 2015-12-07 21:24:14 -05:00
5a716dff16 Add basic multiplexing support. 2015-12-07 16:39:59 -05:00
8ee2716245 Scrub theme.js. 2015-12-07 10:16:38 -05:00
5f19f0a7df Add baseline support for multifile themes. #rough 2015-12-07 09:51:00 -05:00
cf25621679 Introduce placeholder LaTeX generator. 2015-12-06 18:29:16 -05:00
228f14d06c Support recursive theme template loading. 2015-12-06 18:19:33 -05:00
307c37dc44 Use "src" subfolder instead of "templates". 2015-12-06 18:18:36 -05:00
3b8d100f39 Add baseline Handlebars support. 2015-12-06 16:19:55 -05:00
fb783cdbc6 Add Handlebars dependency. 2015-12-06 14:57:20 -05:00
e4d098a3ce Add safety for implicit Markdown. 2015-12-06 05:51:03 -05:00
263f224e1b Bump fluent-themes version to 0.6.0-beta. 2015-12-06 05:50:14 -05:00
92ca11f23c Adjust output. 2015-12-02 15:10:38 -05:00
5b3a25c461 Support NEW command. 2015-12-02 14:56:36 -05:00
2431ae4d89 Bump version to 0.10.0. 2015-12-02 14:56:06 -05:00
2a8f0196b4 Update LICENSE author. 2015-12-01 14:33:49 -05:00
JD
d2791014ef Merge pull request #19 from fluentdesk/rel/v0.9.1
Rel/v0.9.1
2015-11-24 10:52:20 -05:00
e51eb270fc Bump versions. 2015-11-24 10:57:04 -05:00
fbc98060ce Fix theme loading glitch. 2015-11-24 10:56:28 -05:00
af33b6eded Merge branch 'refs/heads/feat/fresh' 2015-11-22 03:19:15 -05:00
51a44ce4a8 Merge remote-tracking branch 'refs/remotes/origin/master' into feat/fresh 2015-11-22 03:19:01 -05:00
JD
80315f12ac Merge pull request #18 from fluentdesk/feat/fresh
Feat/fresh
2015-11-22 03:09:31 -05:00
37225aec84 Update FRESCA version. 2015-11-22 03:03:16 -05:00
39fd689f61 Fix README table glitch. 2015-11-22 02:41:21 -05:00
5899989feb Update README. 2015-11-22 02:37:14 -05:00
42770989bc Tweak colors for Linux. 2015-11-22 00:10:08 -05:00
eade6f3a5c Tweak colors. 2015-11-22 00:07:30 -05:00
0fe334f433 Bump fluent-themes version. 2015-11-21 23:28:33 -05:00
5735ddc495 Multiple enhancements.
A set of rough enhancements supporting FRESH:

- Added ability to process multiple sources for all commands (BUILD,
VALIDATE, CONVERT).

- Added new HELP command to show usage.

- Improved error-handling and color-coding.
2015-11-21 16:12:22 -05:00
992069b22d Cleanup. 2015-11-21 10:33:16 -05:00
cbddb4b3aa Add convenience filter for links. 2015-11-21 09:13:21 -05:00
317de75a5b Refactor. 2015-11-21 07:59:30 -05:00
9fbab27d73 Improve validation and color-coding. 2015-11-21 05:56:16 -05:00
e44239b24a Update package.json deps. 2015-11-21 03:11:37 -05:00
debd866545 Adjust date references. 2015-11-21 03:11:18 -05:00
bf34b01367 Add YUIDoc support 2015-11-21 03:10:11 -05:00
5304cbabd9 Tweak converter. 2015-11-20 15:29:38 -05:00
4de997840e Scrub. 2015-11-20 09:53:36 -05:00
9cde39703e Clean up handling of "meta". 2015-11-20 09:28:55 -05:00
ad6d2c75ca Update FRESH tests. 2015-11-20 08:29:28 -05:00
c14176a504 Implement "convert" command. 2015-11-20 08:29:19 -05:00
16cf97e08e Improve converter. 2015-11-20 08:27:39 -05:00
c96d37b7f1 Update README. 2015-11-19 17:43:54 -05:00
15a74587bc Update FRESH tests with new exemplar name. 2015-11-19 17:19:27 -05:00
1b3fdfbc5f Add FRESH validation test. 2015-11-19 16:24:56 -05:00
0c1b1734ee Update tests. 2015-11-19 15:39:26 -05:00
35b9f2b764 Fix JSON date validation.
JSON "date" type should accept YYYY, YYYY-MM, and YYYY-MM-DD but
is-my-json-valid only validates the last of the three.
2015-11-19 12:36:58 -05:00
87618afa8d Remove unused verb. 2015-11-19 10:39:59 -05:00
458c8519b5 Update FRESH tests with recent changes. 2015-11-19 10:39:48 -05:00
0aa9bc2937 Rename Sheet/FreshSheet to JRSResume/FRESHResume. 2015-11-19 10:39:14 -05:00
a410153253 Implement "generate" and "validate" verbs.
Start moving to a more familiar verb-based interface with "generate" and
"validate" commands. Use with "fluentcv generate" or "fluentcv
validate".
2015-11-19 09:46:02 -05:00
9044dff504 Introduce FRESH and JSONResume conversion routines. 2015-11-19 09:39:49 -05:00
b167abcb78 Bump version to 0.9.0. 2015-11-19 01:57:43 -05:00
ce95593031 Relax copyright notices. 2015-11-19 01:57:15 -05:00
30b6bc4d80 Remove invalid object model reference. 2015-11-19 01:47:23 -05:00
0bebd87bd6 Rename JSON Resume test sheet to test-jrs-sheet.js. 2015-11-18 23:44:38 -05:00
f3eb46a154 Add starter tests for FRESH sheet. 2015-11-18 23:44:16 -05:00
6ce2ae2391 Introduce FRESH sheet class.
Introduce the canonical FRESH sheet class based on the old HackMyResume
(HMR) sources. Prepare to replace JSON Resume-specific handling with
generic FRESH handling.
2015-11-18 23:42:09 -05:00
JD
8cb3e8849e Merge pull request #17 from fluentdesk/ver/0.8.0
ver/0.8.0
2015-11-18 04:56:27 -05:00
07a072f8d7 Bump fluent-themes version. 2015-11-18 04:56:49 -05:00
8a56c61d56 Fix HTML-based PDF generator glitch. 2015-11-07 02:46:12 -05:00
99722e3bd1 Add filename metadata on save. 2015-11-06 14:56:33 -05:00
aa0ef4e8a4 Adjust CSS file handling by generators. 2015-11-05 00:57:57 -05:00
537bd4a7b9 Clear profiles on sheet reset. 2015-11-05 00:57:23 -05:00
9bd41d5825 Don't save "display_progress_bar". 2015-11-05 00:57:10 -05:00
4cc3fd3a1f Comments. 2015-11-05 00:56:41 -05:00
4752c3040e Store theme folder reference. 2015-11-05 00:56:06 -05:00
61c7d6b8f9 Bump version to 0.8.0. 2015-11-05 00:54:23 -05:00
89b7ed4d5b Expose FluentDate at API level. 2015-11-05 00:53:48 -05:00
fdfdd970a7 Bump version. 2015-10-27 21:48:30 -04:00
JD
22bb3252cd Merge pull request #15 from fluentdesk/ver/0.8.0
Bump version.
2015-10-27 21:48:28 -04:00
JD
b4907dc1b9 Merge pull request #14 from fluentdesk/ver/0.8.0
ver/0.7.2
2015-10-27 21:37:35 -04:00
2c6436be5e Bump fluent-themes version. 2015-10-27 21:11:00 -04:00
1e44ce5e5e Fix: Allow "current" in addition to "present" and "now". 2015-10-27 21:09:54 -04:00
9de5069c20 Fix: Allow year-only dates ('YYYY'). 2015-10-27 21:07:45 -04:00
b0b2af8278 Fix glitch with prettyifying HTML links. 2015-10-27 20:59:21 -04:00
JD
f9c4a70ca4 Merge pull request #13 from fluentdesk/ver/0.7.1
Ver/0.7.1
2015-10-27 07:35:40 -04:00
1782d06b37 Bump version. 2015-10-27 07:39:06 -04:00
5dee90b8e3 Remove process.exit() call. 2015-10-27 07:37:24 -04:00
31 changed files with 2509 additions and 522 deletions

1
.gitignore vendored
View File

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

View File

@ -1,7 +1,7 @@
'use strict';
module.exports = function (grunt) {
'use strict';
var opts = {
pkg: grunt.file.readJSON('package.json'),
@ -15,15 +15,42 @@ module.exports = function (grunt) {
reporter: 'spec'
},
all: { src: ['tests/*.js'] }
},
yuidoc: {
compile: {
name: '<%= pkg.name %>',
description: '<%= pkg.description %>',
version: '<%= pkg.version %>',
url: '<%= pkg.homepage %>',
options: {
paths: 'src/',
//themedir: 'path/to/custom/theme/',
outdir: 'docs/'
}
}
},
jshint: {
options: {
laxcomma: true,
expr: true
},
all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.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' ]);
grunt.loadNpmTasks('grunt-contrib-yuidoc');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.registerTask('test', 'Test the FluentCV library.',
function( config ) { grunt.task.run( ['simplemocha:all'] ); });
grunt.registerTask('document', 'Generate FluentCV library documentation.',
function( config ) { grunt.task.run( ['yuidoc'] ); });
grunt.registerTask('default', [ 'jshint', 'test', 'yuidoc' ]);
};

View File

@ -1,7 +1,7 @@
The MIT License
===============
Copyright (c) 2015 James M. Devlin (https://github.com/devlinjd)
Copyright (c) 2015 James M. Devlin (https://github.com/hacksalot)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

203
README.md
View File

@ -1,64 +1,149 @@
fluentCV
========
*Generate beautiful, targeted resumes from your command line or shell.*
FluentCV is a **hackable, data-driven, dev-friendly resume authoring tool** with support for HTML, Markdown, Word, PDF, plain text, smoke signal, carrier pigeon, and other arbitrary-format resumes and CVs.
*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.*
![](assets/fluentcv_cli_ubuntu.png)
Looking for a desktop GUI version with pretty timelines and graphs? Check out [FluentCV Desktop][7].
FluentCV is a dev-friendly 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
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.
## Features
- Runs on OS X, Linux, and Windows.
- Store your resume data as a durable, versionable JSON, YML, or XML document.
- Generate multiple targeted resumes in multiple formats, based on your needs.
- Output to HTML, PDF, Markdown, Word, JSON, YAML, XML, or a custom format.
- Never update one piece of information in four different resumes again.
- Compatible with the [JSON Resume standard][6] and [authoring tools][7].
- OS X, Linux, and Windows.
- 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].
- Free and open-source through the MIT license.
- Forthcoming: StackOverflow and LinkedIn support.
- Forthcoming: More themes!
## Install
Install FluentCV with NPM:
```bash
[sudo] npm install fluentcv -g
```
Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for
your platform. For LaTeX generation you'll need a valid LaTeX environment with
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:
- `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
```
- `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
```
- `convert` converts your source resume between FRESH and JSON Resume formats.
Use it to convert between the two formats to take advantage of tools and services.
```bash
# fluentcv CONVERT <INPUTS> TO <OUTPUTS>
fluentcv CONVERT resume.json TO resume-jrs.json
fluentcv CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json
```
- `validate` validates the specified resume against either the FRESH or JSON
Resume schema. Use it to make sure your resume data is sufficient and complete.
```bash
# fluentcv VALIDATE <INPUTS>
fluentcv VALIDATE resume.json
fluentcv VALIDATE r1.json r2.json r3.json
```
## Supported Output Formats
FluentCV supports these output formats:
Output Format | Ext | Notes
------------- | --- | -----
HTML | .html | A standard HTML 5 + CSS resume format that can be viewed in a browser, deployed to a website, etc.
Markdown | .md | A structured Markdown document that can be used as-is or used to generate HTML.
LaTeX | .tex | A structured LaTeX document (or collection of documents).
MS Word | .doc | A Microsoft Word office document.
Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme.
plain text | .txt | A formatted plain text document appropriate for emails or copy-paste.
JSON | .json | A JSON representation of the resume.
YAML | .yml | A YAML representation of the resume.
RTF | .rtf | Forthcoming.
Textile | .textile | Forthcoming.
image | .png, .bmp | Forthcoming.
## Install
FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then:
1. (Optional, for PDF support) Install the latest official [wkhtmltopdf][3] binary for your platform.
2. Install **fluentCV** by running `npm install fluentcv -g`.
1. Install the latest official [wkhtmltopdf][3] binary for your platform.
2. Optionally install an updated LaTeX environment (LaTeX resumes only).
2. Install **fluentCV** with `[sudo] npm install fluentcv -g`.
3. You're ready to go.
## Use
Assuming you've got a JSON-formatted resume handy, generating resumes in different formats and combinations easy. Just run:
Assuming you've got a JSON-formatted resume handy, generating resumes in
different formats and combinations easy. Just run:
```bash
fluentcv [inputs] [outputs] [-t theme].
fluentcv BUILD <INPUTS> <OUTPUTS> [-t theme].
```
Where `[inputs]` is one or more .json resume files, separated by spaces; `[outputs]` is one or more destination resumes, each prefaced with the `-o` option; and `[theme]` is the desired theme. For example:
Where `<INPUTS>` is one or more .json resume files, separated by spaces;
`<OUTPUTS>` is one or more destination resumes, and `<THEME>` is the desired
theme (default to Modern). For example:
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.)
fluentcv resume.json -o out/resume.all -t modern
fluentcv build resume.json -o out/resume.all -t modern
# Generate a specific resume format
fluentcv resume.json -o out/resume.html
fluentcv resume.json -o out/resume.pdf
fluentcv resume.json -o out/resume.md
fluentcv resume.json -o out/resume.doc
fluentcv resume.json -o out/resume.json
fluentcv resume.json -o out/resume.txt
fluentcv resume.json -o out/resume.yml
fluentcv build resume.json TO out/resume.html
fluentcv build resume.json TO out/resume.pdf
fluentcv build resume.json TO out/resume.md
fluentcv build resume.json TO out/resume.doc
fluentcv build resume.json TO out/resume.json
fluentcv build resume.json TO out/resume.txt
fluentcv build resume.json TO out/resume.yml
# Specify 2 inputs and 3 outputs
fluentcv in1.json in2.json -o out.html -o out.doc -o out.pdf
fluentcv build in1.json in2.json TO out.html out.doc out.pdf
```
You should see something to the effect of:
```
*** FluentCV v0.7.0 ***
*** FluentCV v0.9.0 ***
Reading JSON resume: foo/resume.json
Applying MODERN Theme (7 formats)
Generating HTML resume: out/resume.html
@ -77,11 +162,11 @@ Generating YAML resume: out/resume.yml
You can specify a predefined or custom theme via the optional `-t` parameter. For a predefined theme, include the theme name. For a custom theme, include the path to the custom theme's folder.
```bash
fluentcv resume.json -t modern
fluentcv resume.json -t ~/foo/bar/my-custom-theme/
fluentcv build resume.json -t modern
fluentcv build resume.json -t ~/foo/bar/my-custom-theme/
```
As of v0.7.0, available predefined themes are `modern`, `minimist`, and `hello-world`.
As of v0.9.0, available predefined themes are `modern`, `minimist`, and `hello-world`, and `compact`.
### Merging resumes
@ -89,13 +174,13 @@ You can **merge multiple resumes together** by specifying them in order from mos
```bash
# Merge specific.json onto base.json and generate all formats
fluentcv base.json specific.json -o resume.all
fluentcv build base.json specific.json -o resume.all
```
This can be useful for overriding a base (generic) resume with information from a specific (targeted) resume. For example, you might override your generic catch-all "software developer" resume with specific details from your targeted "game developer" resume, or combine two partial resumes into a "complete" resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge:
```bash
fluentcv in1.json in2.json in3.json in4.json -o out.html -o out.doc
fluentcv build in1.json in2.json in3.json in4.json TO out.html out.doc
Reading JSON resume: in1.json
Reading JSON resume: in2.json
Reading JSON resume: in3.json
@ -111,14 +196,14 @@ You can specify **multiple output targets** and FluentCV will build them:
```bash
# Generate out1.doc, out1.pdf, and foo.txt from me.json.
fluentcv me.json -o out1.doc -o out1.pdf -o foo.txt
fluentcv build me.json -o out1.doc -o out1.pdf -o foo.txt
```
You can also omit the output file(s) and/or theme completely:
```bash
# Equivalent to "fluentcv resume.json resume.all -t modern"
fluentcv resume.json
fluentcv build resume.json
```
### Using .all
@ -127,17 +212,52 @@ The special `.all` extension tells FluentCV to generate all supported output for
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, etc.)
fluentcv me.json -o out/resume.all
fluentcv build me.json -o out/resume.all
```
..tells FluentCV to read `me.json` and generate `out/resume.md`, `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`.
### Prettifying
### Validating
FluentCV applies [js-beautify][10]-style HTML prettification by default to HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag can be used:
FluentCV can also validate your resumes against either the [FRESH /
FRESCA][fresca] or [JSON Resume][6] formats. To validate one or more existing
resumes, use the `validate` command:
```bash
fluentcv resume.json out.all --nopretty
# Validate myresume.json against either the FRESH or JSON Resume schema.
fluentcv validate resumeA.json resumeB.json
```
FluentCV will validate each specified resume in turn:
```bash
*** FluentCV v0.9.0 ***
Validating JSON resume: resumeA.json (INVALID)
Validating JSON resume: resumeB.json (VALID)
```
### Converting
FluentCV can convert between the [FRESH][fresca] and [JSON Resume][6] formats.
Just run:
```bash
fluentcv CONVERT <INPUTS> <OUTPUTS>
```
where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and
<OUTPUTS> is a corresponding list of output file names. FluentCV will autodetect
the format (FRESH or JRS) of each input resume and convert it to the other
format (JRS or FRESH).
### Prettifying
FluentCV applies [js-beautify][10]-style HTML prettification by default to
HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag
can be used:
```bash
fluentcv generate resume.json out.all --nopretty
```
### Silent Mode
@ -145,8 +265,8 @@ fluentcv resume.json out.all --nopretty
Use `-s` or `--silent` to run in silent mode:
```bash
fluentcv resume.json -o someFile.all -s
fluentcv resume.json -o someFile.all --silent
fluentcv generate resume.json -o someFile.all -s
fluentcv generate resume.json -o someFile.all --silent
```
## License
@ -163,3 +283,6 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
[8]: https://youtu.be/N9wsjroVlu8
[9]: https://api.jquery.com/jquery.extend/
[10]: https://github.com/beautify-web/js-beautify
[fresh]: https://github.com/fluentdesk/FRESH
[fresca]: https://github.com/fluentdesk/FRESCA
[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1,6 +1,6 @@
{
"name": "fluentcv",
"version": "0.7.0",
"version": "0.10.0",
"description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.",
"repository": {
"type": "git",
@ -10,7 +10,15 @@
"resume",
"CV",
"portfolio",
"Markdown"
"employment",
"career",
"Markdown",
"JSON",
"Word",
"PDF",
"YAML",
"HTML",
"CLI"
],
"author": "James M. Devlin",
"license": "MIT",
@ -24,8 +32,11 @@
},
"homepage": "https://github.com/fluentdesk/fluentcv",
"dependencies": {
"fluent-themes": "0.1.0-beta",
"fresca": "^0.2.0",
"colors": "^1.1.2",
"fluent-themes": "0.6.0-beta",
"fs-extra": "^0.24.0",
"handlebars": "^4.0.5",
"html": "0.0.10",
"is-my-json-valid": "^2.12.2",
"jst": "0.0.13",
@ -33,6 +44,7 @@
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"moment": "^2.10.6",
"recursive-readdir-sync": "^1.0.6",
"underscore": "^1.8.3",
"wkhtmltopdf": "^0.1.5",
"xml-escape": "^1.0.0",
@ -41,6 +53,8 @@
"devDependencies": {
"chai": "*",
"grunt": "*",
"grunt-contrib-jshint": "^0.11.3",
"grunt-contrib-yuidoc": "^0.10.0",
"grunt-simple-mocha": "*",
"is-my-json-valid": "^2.12.2",
"mocha": "*",

291
src/core/convert.js Normal file
View File

@ -0,0 +1,291 @@
/**
FRESH to JSON Resume conversion routiens.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
*/
(function(){
/**
Convert between FRESH and JRS resume/CV formats.
@class FRESHConverter
*/
var FRESHConverter = module.exports = {
/**
Convert from JSON Resume format to FRESH.
*/
toFRESH: function( src, foreign ) {
foreign = (foreign === undefined || foreign === null) ? true : foreign;
return {
name: src.basics.name,
info: {
label: src.basics.label,
class: src.basics.class, // <--> round-trip
image: src.basics.picture,
brief: src.basics.summary
},
contact: {
email: src.basics.email,
phone: src.basics.phone,
website: src.basics.website,
other: src.basics.other // <--> round-trip
},
meta: meta( true, src.meta ),
location: {
city: src.basics.location.city,
region: src.basics.location.region,
country: src.basics.location.countryCode,
code: src.basics.location.postalCode,
address: src.basics.location.address
},
employment: {
history: src.work.map( function( job ) {
return {
position: job.position,
employer: job.company,
summary: job.summary,
current: (!job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current') || undefined,
start: job.startDate,
end: job.endDate,
url: job.website,
keywords: "",
highlights: job.highlights
};
})
},
education: {
history: src.education.map(function(edu){
return {
institution: edu.institution,
start: edu.startDate,
end: edu.endDate,
grade: edu.gpa,
curriculum: edu.courses,
url: edu.website || edu.url || null,
summary: null,
area: edu.area,
studyType: edu.studyType
};
})
},
service: {
history: src.volunteer.map(function(vol) {
return {
type: 'volunteer',
position: vol.position,
organization: vol.organization,
start: vol.startDate,
end: vol.endDate,
url: vol.website,
summary: vol.summary,
highlights: vol.highlights
};
})
},
skills: skillsToFRESH( src.skills ),
writing: src.publications.map(function(pub){
return {
title: pub.name,
flavor: undefined,
publisher: pub.publisher,
url: pub.website,
date: pub.releaseDate,
summary: pub.summary
};
}),
recognition: src.awards.map(function(awd){
return {
title: awd.title,
date: awd.date,
summary: awd.summary,
from: awd.awarder,
url: null
};
}),
social: src.basics.profiles.map(function(pro){
return {
label: pro.network,
network: pro.network,
url: pro.url,
user: pro.username
};
}),
interests: src.interests,
references: src.references,
languages: src.languages,
disposition: src.disposition // <--> round-trip
};
},
/**
Convert from FRESH format to JSON Resume.
@param foreign True if non-JSON-Resume properties should be included in
the result, false if those properties should be excluded.
*/
toJRS: function( src, foreign ) {
foreign = (foreign === undefined || foreign === null) ? false : foreign;
return {
basics: {
name: src.name,
label: src.info.label,
class: foreign ? src.info.class : undefined,
summary: src.info.brief,
website: src.contact.website,
phone: src.contact.phone,
email: src.contact.email,
picture: src.info.image,
location: {
address: src.location.address,
postalCode: src.location.code,
city: src.location.city,
countryCode: src.location.country,
region: src.location.region
},
profiles: src.social.map(function(soc){
return {
network: soc.network,
username: soc.user,
url: soc.url
};
})
},
work: src.employment.history.map(function(emp){
return {
company: emp.employer,
website: emp.url,
position: emp.position,
startDate: emp.start,
endDate: emp.end,
summary: emp.summary,
highlights: emp.highlights
};
}),
education: src.education.history.map(function(edu){
return {
institution: edu.institution,
gpa: edu.grade,
courses: edu.curriculum,
startDate: edu.start,
endDate: edu.end,
area: edu.area,
studyType: edu.studyType
};
}),
skills: skillsToJRS( src.skills ),
volunteer: src.service.history.map(function(srv){
return {
flavor: foreign ? srv.flavor : undefined,
organization: srv.organization,
position: srv.position,
startDate: srv.start,
endDate: srv.end,
website: srv.url,
summary: srv.summary,
highlights: srv.highlights
};
}),
awards: src.recognition.map(function(awd){
return {
flavor: foreign ? awd.flavor : undefined,
url: foreign ? awd.url: undefined,
title: awd.title,
date: awd.date,
awarder: awd.from,
summary: awd.summary
};
}),
publications: src.writing.map(function(pub){
return {
name: pub.title,
publisher: pub.publisher,
releaseDate: pub.date,
website: pub.url,
summary: pub.summary
};
}),
interests: src.interests,
references: src.references,
samples: foreign ? src.samples : undefined,
disposition: foreign ? src.disposition : undefined,
languages: src.languages
};
}
};
function meta( direction, obj ) {
if( direction ) {
obj = obj || { };
obj.format = obj.format || "FRESH@0.1.0";
obj.version = obj.version || "0.1.0";
}
return obj;
}
function skillsToFRESH( skills ) {
return {
sets: skills.map(function(set) {
return {
name: set.name,
level: set.level,
skills: set.keywords
};
})
};
}
function skillsToJRS( skills ) {
var ret = [];
if( skills.sets && skills.sets.length ) {
ret = skills.sets.map(function(set){
return {
name: set.name,
level: set.level,
keywords: set.skills
};
});
}
else if( skills.list ) {
ret = skills.list.map(function(sk){
return {
name: sk.name,
level: sk.level,
keywords: sk.keywords
};
});
}
return ret;
}
}());

182
src/core/empty-fresh.json Normal file
View File

@ -0,0 +1,182 @@
{
"name": "",
"meta": {
"format": "FRESH@0.1.0",
"version": "0.1.0"
},
"info": {
"label": "",
"characterClass": "",
"brief": "",
"image": ""
},
"contact": {
"website": "",
"phone": "",
"email": "",
"other": []
},
"location": {
"address": "",
"city": "",
"region": "",
"code": "",
"country": ""
},
"social": [
{
"label": "",
"network": "",
"user": "",
"url": ""
}
],
"employment": {
"summary": "",
"history": [
{
"employer": "",
"url": "",
"position": "",
"summary": "",
"start": "",
"keywords": [],
"highlights": []
}
]
},
"education": {
"summary": "",
"level": "",
"degree": "",
"history": [
{
"institution": "",
"url": "",
"start": "",
"end": "",
"grade": "",
"summary": "",
"curriculum": []
}
]
},
"service": {
"summary": "",
"history": [
{
"flavor": "",
"position": "",
"organization": "",
"url": "",
"start": "",
"end": "",
"summary": "",
"highlights": []
}
]
},
"skills": {
"sets": [
{
"name": "",
"skills": []
}
],
"list": [ ]
},
"samples": [
{
"title": "",
"summary": "",
"url": "",
"date": ""
}
],
"writing": [
{
"title": "",
"flavor": "",
"date": "",
"publisher": {
"name": "",
"url": ""
},
"url": ""
}
],
"reading": [
{
"title": "",
"flavor": "",
"url": "",
"author": ""
}
],
"recognition": [
{
"flavor": "",
"from": "",
"title": "",
"event": "",
"date": "",
"summary": ""
}
],
"references": [
{
"name": "",
"flavor": "",
"private": true,
"contact": [
{
"label": "",
"flavor": "",
"value": ""
}
]
}
],
"testimonials": [
{
"name": "",
"flavor": "",
"quote": ""
}
],
"languages": [
{
"language": "",
"level": "",
"years": 0
}
],
"interests": [
{
"name": "",
"summary": "",
"keywords": []
}
]
}

View File

@ -28,19 +28,19 @@ function FluentDate( dt ) {
FluentDate/*.prototype*/.fmt = function( dt ) {
if( (typeof dt === 'string' || dt instanceof String) ) {
dt = dt.toLowerCase().trim();
if( /^(present|now)$/.test(dt) ) { // "Present", "Now"
if( /^(present|now|current)$/.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' );
var temp = parts[1] + '-' + (month < 10 ? '0' + month : month.toString());
return moment( temp, '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"
else if( /^\s*\d{4}\s*$/.test(dt) ) { // "2015"
return moment( dt, 'YYYY' );
}
else if( /^\s*$/.test(dt) ) { // "", " "

397
src/core/fresh-resume.js Normal file
View File

@ -0,0 +1,397 @@
/**
Definition of the FRESHResume class.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
*/
(function() {
var FS = require('fs')
, extend = require('../utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, PATH = require('path')
, moment = require('moment')
, MD = require('marked')
, CONVERTER = require('./convert');
/**
A FRESH-style resume in JSON or YAML.
@class FreshResume
*/
function FreshResume() {
}
/**
Open and parse the specified FRESH resume sheet. Merge the JSON object model
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
*/
FreshResume.prototype.open = function( file, title ) {
this.imp = { fileName: file };
this.imp.raw = FS.readFileSync( file, 'utf8' );
return this.parse( this.imp.raw, title );
};
/**
Save the sheet to disk (for environments that have disk access).
*/
FreshResume.prototype.save = function( filename ) {
this.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
return this;
};
/**
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
FreshResume.prototype.saveAs = function( filename, format ) {
this.imp.fileName = filename || this.imp.fileName;
if( format !== 'JRS' ) {
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
}
else {
var newRep = CONVERTER.toJRS( this );
FS.writeFileSync( this.imp.fileName, FreshResume.stringify( newRep ), 'utf8' );
}
return this;
};
FreshResume.prototype.dupe = function() {
var rnew = new FreshResume();
rnew.parse( this.stringify(), { } );
return rnew;
};
/**
Convert the supplied object to a JSON string, sanitizing meta-properties along
the way.
*/
FreshResume.stringify = function( obj ) {
function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
return JSON.stringify( obj, replacer, 2 );
};
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
FreshResume.prototype.markdownify = function() {
var that = this;
var ret = this.dupe();
function MDIN(txt){
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
// TODO: refactor recursion
function markdownifyStringsInObject( obj, inline ) {
if( !obj ) return;
inline = inline === undefined || inline;
if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
obj.forEach(function(elem, idx, ar){
if( typeof elem === 'string' || elem instanceof String )
ar[idx] = inline ? MDIN(elem) : MD( elem );
else
markdownifyStringsInObject( elem );
});
}
else if (typeof obj === 'object') {
Object.keys( obj ).forEach(function(key) {
var sub = obj[key];
if( typeof sub === 'string' || sub instanceof String ) {
if( _.contains(['skills','url','start','end','date'], key) )
return;
if( key === 'summary' )
obj[key] = MD( obj[key] );
else
obj[key] = inline ? MDIN( obj[key] ) : MD( obj[key] );
}
else
markdownifyStringsInObject( sub );
});
}
}
Object.keys( ret ).forEach(function(member){
markdownifyStringsInObject( ret[ member ] );
});
return ret;
};
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
FreshResume.prototype.stringify = function() {
return FreshResume.stringify( this );
};
/**
Open and parse the specified JSON resume sheet. Merge the JSON object model
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
*/
FreshResume.prototype.parse = function( stringData, opts ) {
// Parse the incoming JSON representation
var rep = JSON.parse( stringData );
// Convert JSON Resume to FRESH if necessary
if( rep.basics ) {
rep = CONVERTER.toFRESH( rep );
rep.imp = rep.imp || { };
rep.imp.orgFormat = 'JRS';
}
// Now apply the resume representation onto this object
extend( true, this, rep );
// Set up metadata
opts = opts || { };
if( opts.imp === undefined || opts.imp ) {
this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.name;
}
// Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this );
(opts.sort === undefined || opts.sort) && this.sort();
(opts.compute === undefined || opts.compute) && (this.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
return this;
};
/**
Return a unique list of all keywords across all skills.
*/
FreshResume.prototype.keywords = function() {
var flatSkills = [];
this.skills && this.skills.length &&
(flatSkills = this.skills.map(function(sk) { return sk.name; }));
return flatSkills;
},
/**
Update the sheet's raw data. TODO: remove/refactor
*/
FreshResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str );
return this;
};
/**
Reset the sheet to an empty state.
*/
FreshResume.prototype.clear = function( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
clearMeta && (delete this.imp);
delete this.computed; // Don't use Object.keys() here
delete this.employment;
delete this.service;
delete this.education;
delete this.recognition;
delete this.reading;
delete this.writing;
delete this.interests;
delete this.skills;
delete this.social;
};
/**
Get the default (empty) sheet.
*/
FreshResume.default = function() {
return new FreshResume().open(
PATH.join( __dirname, 'empty-fresh.json'), 'Empty' );
};
/**
Add work experience to the sheet.
*/
FreshResume.prototype.add = function( moniker ) {
var defSheet = FreshResume.default();
var newObject = $.extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject );
return newObject;
};
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
FreshResume.prototype.hasProfile = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.some( this.social, function(p) {
return p.network.trim().toLowerCase() === socialNetwork;
});
};
/**
Return the specified network profile.
*/
FreshResume.prototype.getProfile = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.find( this.social, function(sn) {
return sn.network.trim().toLowerCase() === socialNetwork;
});
};
/**
Return an array of profiles for the specified network, for when the user
has multiple eg. GitHub accounts.
*/
FreshResume.prototype.getProfiles = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.filter( this.social, function(sn){
return sn.network.trim().toLowerCase() === socialNetwork;
});
};
/**
Determine if the sheet includes a specific skill.
*/
FreshResume.prototype.hasSkill = function( skill ) {
skill = skill.trim().toLowerCase();
return this.skills && _.some( this.skills, function(sk) {
return sk.keywords && _.some( sk.keywords, function(kw) {
return kw.trim().toLowerCase() === skill;
});
});
};
/**
Validate the sheet against the FRESH Resume schema.
*/
FreshResume.prototype.isValid = function( info ) {
var schemaObj = require('FRESCA');
var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
var ret = validate( this );
if( !ret ) {
this.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
}
return ret;
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
FreshResume.prototype.duration = function() {
if( this.employment.history && this.employment.history.length ) {
var careerStart = this.employment.history[ this.employment.history.length - 1].safe.start;
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
!careerStart.trim())
return 0;
var careerLast = _.max( this.employment.history, function( w ) {
return w.safe.end.unix();
}).safe.end;
return careerLast.diff( careerStart, 'years' );
}
return 0;
};
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
FreshResume.prototype.sort = function( ) {
this.employment.history && this.employment.history.sort( byDateDesc );
this.education.history && this.education.history.sort( byDateDesc );
this.service.history && this.service.history.sort( byDateDesc );
// this.awards && this.awards.sort( function(a, b) {
// return( a.safeDate.isBefore(b.safeDate) ) ? 1
// : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
// });
this.writing && this.writing.sort( function(a, b) {
return( a.safe.date.isBefore(b.safe.date) ) ? 1
: ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0;
});
function byDateDesc(a,b) {
return( a.safe.start.isBefore(b.safe.start) ) ? 1
: ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0;
}
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
function _parseDates() {
var _fmt = require('./fluent-date').fmt;
var that = this;
// TODO: refactor recursion
function replaceDatesInObject( obj ) {
if( !obj ) return;
if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
obj.forEach(function(elem){
replaceDatesInObject( elem );
});
}
else if (typeof obj === 'object') {
if( obj._isAMomentObject || obj.safe )
return;
Object.keys( obj ).forEach(function(key) {
replaceDatesInObject( obj[key] );
});
['start','end','date'].forEach( function(val) {
if( obj[val] && (!obj.safe || !obj.safe[val] )) {
obj.safe = obj.safe || { };
obj.safe[ val ] = _fmt( obj[val] );
if( obj[val] && (val === 'start') && !obj.end ) {
obj.safe.end = _fmt('current');
}
}
});
}
}
Object.keys( this ).forEach(function(member){
replaceDatesInObject( that[ member ] );
});
}
/**
Export the Sheet function/ctor.
*/
module.exports = FreshResume;
}());
// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
// in addition to YYYY-MM-DD. The original regex:
//
// /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/
//

View File

@ -1,6 +1,6 @@
/**
Abstract character/resume sheet representation.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the JRSResume class.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
*/
(function() {
@ -13,14 +13,14 @@ Abstract character/resume sheet representation.
, moment = require('moment');
/**
The Sheet class represent a specific JSON character sheet. When Sheet.open
The JRSResume class represent a specific JSON character sheet. When Sheet.open
is called, we merge the loaded JSON sheet properties onto the Sheet instance
via extend(), so a full-grown sheet object will have all of the methods here,
plus a complement of JSON properties from the backing JSON file. That allows
us to treat Sheet objects interchangeably with the loaded JSON model.
@class Sheet
@class JRSResume
*/
function Sheet() {
function JRSResume() {
}
@ -29,18 +29,18 @@ Abstract character/resume sheet representation.
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
*/
Sheet.prototype.open = function( file, title ) {
this.meta = { fileName: file };
this.meta.raw = FS.readFileSync( file, 'utf8' );
return this.parse( this.meta.raw, title );
JRSResume.prototype.open = function( file, title ) {
this.imp = { fileName: file };
this.imp.raw = FS.readFileSync( file, 'utf8' );
return this.parse( this.imp.raw, title );
};
/**
Save the sheet to disk (for environments that have disk access).
*/
Sheet.prototype.save = function( filename ) {
filename = filename || this.meta.fileName;
FS.writeFileSync( filename, this.stringify(), 'utf8' );
JRSResume.prototype.save = function( filename ) {
this.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
return this;
};
@ -48,11 +48,11 @@ Abstract character/resume sheet representation.
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
Sheet.prototype.stringify = function() {
JRSResume.prototype.stringify = function() {
function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index',
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview'],
'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
@ -64,14 +64,14 @@ Abstract character/resume sheet representation.
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
*/
Sheet.prototype.parse = function( stringData, opts ) {
JRSResume.prototype.parse = function( stringData, opts ) {
opts = opts || { };
var rep = JSON.parse( stringData );
extend( true, this, rep );
// Set up metadata
if( opts.meta === undefined || opts.meta ) {
this.meta = this.meta || { };
this.meta.title = (opts.title || this.meta.title) || this.basics.name;
if( opts.imp === undefined || opts.imp ) {
this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.basics.name;
}
// Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this );
@ -86,7 +86,7 @@ Abstract character/resume sheet representation.
/**
Return a unique list of all keywords across all skills.
*/
Sheet.prototype.keywords = function() {
JRSResume.prototype.keywords = function() {
var flatSkills = [];
if( this.skills && this.skills.length ) {
this.skills.forEach( function( s ) {
@ -94,23 +94,23 @@ Abstract character/resume sheet representation.
});
}
return flatSkills;
},
};
/**
Update the sheet's raw data. TODO: remove/refactor
*/
Sheet.prototype.updateData = function( str ) {
JRSResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str )
this.parse( str );
return this;
};
/**
Reset the sheet to an empty state.
*/
Sheet.prototype.clear = function( clearMeta ) {
JRSResume.prototype.clear = function( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
clearMeta && (delete this.meta);
clearMeta && (delete this.imp);
delete this.computed; // Don't use Object.keys() here
delete this.work;
delete this.volunteer;
@ -119,20 +119,21 @@ Abstract character/resume sheet representation.
delete this.publications;
delete this.interests;
delete this.skills;
delete this.basics.profiles;
};
/**
Get the default (empty) sheet.
*/
Sheet.default = function() {
return new Sheet().open( PATH.join( __dirname, 'empty.json'), 'Empty' );
}
JRSResume.default = function() {
return new JRSResume().open( PATH.join( __dirname, 'empty-jrs.json'), 'Empty' );
};
/**
Add work experience to the sheet.
*/
Sheet.prototype.add = function( moniker ) {
var defSheet = Sheet.default();
JRSResume.prototype.add = function( moniker ) {
var defSheet = JRSResume.default();
var newObject = $.extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject );
@ -142,7 +143,7 @@ Abstract character/resume sheet representation.
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
Sheet.prototype.hasProfile = function( socialNetwork ) {
JRSResume.prototype.hasProfile = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.basics.profiles && _.some( this.basics.profiles, function(p) {
return p.network.trim().toLowerCase() === socialNetwork;
@ -152,7 +153,7 @@ Abstract character/resume sheet representation.
/**
Determine if the sheet includes a specific skill.
*/
Sheet.prototype.hasSkill = function( skill ) {
JRSResume.prototype.hasSkill = function( skill ) {
skill = skill.trim().toLowerCase();
return this.skills && _.some( this.skills, function(sk) {
return sk.keywords && _.some( sk.keywords, function(kw) {
@ -164,10 +165,10 @@ Abstract character/resume sheet representation.
/**
Validate the sheet against the JSON Resume schema.
*/
Sheet.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓
JRSResume.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓
var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' );
var schemaObj = JSON.parse( schema );
var validator = require('is-my-json-valid')
var validator = require('is-my-json-valid');
var validate = validator( schemaObj );
return validate( this );
};
@ -180,7 +181,7 @@ Abstract character/resume sheet representation.
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
Sheet.prototype.duration = function() {
JRSResume.prototype.duration = function() {
if( this.work && this.work.length ) {
var careerStart = this.work[ this.work.length - 1].safeStartDate;
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
@ -198,7 +199,7 @@ Abstract character/resume sheet representation.
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
Sheet.prototype.sort = function( ) {
JRSResume.prototype.sort = function( ) {
this.work && this.work.sort( byDateDesc );
this.education && this.education.sort( byDateDesc );
@ -252,8 +253,8 @@ Abstract character/resume sheet representation.
}
/**
Export the Sheet function/ctor.
Export the JRSResume function/ctor.
*/
module.exports = Sheet;
module.exports = JRSResume;
}());

View File

@ -10,10 +10,12 @@ Abstract theme representation.
, validator = require('is-my-json-valid')
, _ = require('underscore')
, PATH = require('path')
, moment = require('moment');
, EXTEND = require('../utils/extend')
, moment = require('moment')
, RECURSIVE_READ_DIR = require('recursive-readdir-sync');
/**
The Theme class represents a specific presentation of a resume.
The Theme class is a representation of a FluentCV theme asset.
@class Theme
*/
function Theme() {
@ -25,49 +27,39 @@ Abstract theme representation.
*/
Theme.prototype.open = function( themeFolder ) {
function friendlyName( val ) {
val = val.trim().toLowerCase();
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val;
// Open the [theme-name].json file; should have the same name as folder
this.folder = themeFolder;
var pathInfo = PATH.parse( themeFolder );
var themeFile = PATH.join( themeFolder, pathInfo.base + '.json' );
var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) );
var that = this;
// Move properties from the theme JSON file to the theme object
EXTEND( true, this, themeInfo );
// Set up a formats has for the theme
var formatsHash = { };
// Check for an explicit "formats" entry in the theme JSON. If it has one,
// then this theme declares its files explicitly.
if( !!this.formats ) {
formatsHash = loadExplicit.call( this );
this.explicit = true;
}
else {
formatsHash = loadImplicit.call( this );
}
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;
});
// Add freebie formats every theme gets
formatsHash.json = { title: 'json', outFormat: 'json', pre: 'json', ext: 'json', path: null, data: null };
formatsHash.yml = { title: 'yaml', outFormat: 'yml', pre: 'yml', ext: 'yml', path: null, data: null };
// 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 } ] );
// Cache
this.formats = formatsHash;
// 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';
});
// Set the official theme name
this.name = PATH.parse( this.folder ).name;
// Create a hash out of the formats for this theme
this.formats = _.object( fmts );
this.name = PATH.parse( themeFolder ).name;
return this;
};
@ -85,6 +77,187 @@ Abstract theme representation.
return this.formats[ fmt ];
};
function loadImplicit() {
// Set up a hash of formats supported by this theme.
var formatsHash = { };
var that = this;
var major = false;
// Establish the base theme folder
var tplFolder = PATH.join( this.folder, 'src' );
// Iterate over all files in the theme folder, producing an array, fmts,
// containing info for each file. While we're doing that, also build up
// the formatsHash object.
var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) {
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
var pathInfo = PATH.parse(absPath);
var outFmt = '', isMajor = false;
var portion = pathInfo.dir.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
var res = reg.exec( portion );
res && (outFmt = res[1]);
}
// Otherwise, the output format is inferred from the filename, as in
// compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-');
outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 );
isMajor = true;
}
// We should have a valid output format now.
formatsHash[ outFmt ] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
};
// Create the file representation object.
var obj = {
action: 'transform',
path: absPath,
major: isMajor,
orgPath: PATH.relative(that.folder, absPath),
ext: pathInfo.ext.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
data: FS.readFileSync( absPath, 'utf8' ),
css: null
};
// Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj );
return obj;
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){ return 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';
});
cssf.action = null;
fmts[ idx ].css = cssf.data;
fmts[ idx ].cssPath = cssf.path;
});
// Remove CSS files from the formats array
fmts = fmts.filter( function( fmt) {
return fmt.ext !== 'css';
});
return formatsHash;
}
function loadExplicit() {
var that = this;
// Set up a hash of formats supported by this theme.
var formatsHash = { };
// Establish the base theme folder
var tplFolder = PATH.join( this.folder, 'src' );
var act = null;
// Iterate over all files in the theme folder, producing an array, fmts,
// containing info for each file. While we're doing that, also build up
// the formatsHash object.
var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) {
act = null;
// If this file is mentioned in the theme's JSON file under "transforms"
var pathInfo = PATH.parse(absPath);
var absPathSafe = absPath.trim().toLowerCase();
var outFmt = _.find( Object.keys( that.formats ), function( fmtKey ) {
var fmtVal = that.formats[ fmtKey ];
return _.some( fmtVal.transform, function( fpath ) {
var absPathB = PATH.join( that.folder, fpath ).trim().toLowerCase();
return absPathB === absPathSafe;
});
});
if( outFmt ) {
act = 'transform';
}
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
if( !outFmt ) {
var portion = pathInfo.dir.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
var res = reg.exec( portion );
res && (outFmt = res[1]);
}
}
// Otherwise, the output format is inferred from the filename, as in
// compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-');
outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 );
}
// We should have a valid output format now.
formatsHash[ outFmt ] =
formatsHash[ outFmt ] || {
outFormat: outFmt,
files: [],
symLinks: that.formats[ outFmt ].symLinks
};
// Create the file representation object.
var obj = {
action: act,
orgPath: PATH.relative(that.folder, absPath),
path: absPath,
ext: pathInfo.ext.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
data: FS.readFileSync( absPath, 'utf8' ),
css: null
};
// Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj );
return obj;
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){ return 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';
});
fmts[ idx ].css = cssf.data;
fmts[ idx ].cssPath = cssf.path;
});
// Remove CSS files from the formats array
fmts = fmts.filter( function( fmt) {
return fmt.ext !== 'css';
});
return formatsHash;
}
function friendlyName( val ) {
val = val.trim().toLowerCase();
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val;
}
module.exports = Theme;
}());

View File

@ -0,0 +1,18 @@
/**
Handlebars template generate for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
*/
(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 || '' } );
};
}());

View File

@ -0,0 +1,37 @@
/**
Underscore template generate for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
*/
(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");
});
}
_.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,148 +1,348 @@
/**
Internal resume generation logic for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@module fluentcmd.js
*/
module.exports = function () {
(function() {
module.exports = function () {
// We don't mind pseudo-globals here
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')
, rez, _log, _err;
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 gen( src, dst, opts, logger, errHandler ) {
/**
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;
_log = logger || console.log;
_err = errHandler || error;
//_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
//_opts = 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 );
});
// 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 ' + a.meta.fileName : '')
+ ' onto ' + b.meta.fileName;
return extend( true, b, a );
});
msg && _log(msg);
// 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);
// 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 );
// 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 )) {
throw { fluenterror: 1, data: _opts.theme };
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 );
}
}
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' ?
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( 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( fi, theme ) {
try {
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 );
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
catch( ex ) {
_err( 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 );
});
});
}
}
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
/**
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() }
];
/**
Default FluentCMD 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
/**
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 );
});
}
};
/**
Internal module interface. Used by FCV Desktop and HMR.
*/
return {
generate: gen,
lib: require('./fluentlib'),
options: _opts,
formats: _fmts
};
/**
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,11 +1,14 @@
/**
Core resume generation module for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
External API surface for FluentCV:CLI.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
*/
module.exports = {
Sheet: require('./core/sheet'),
Sheet: require('./core/fresh-resume'),
FRESHResume: require('./core/fresh-resume'),
JRSResume: require('./core/jrs-resume'),
Theme: require('./core/theme'),
FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./gen/html-generator'),
TextGenerator: require('./gen/text-generator'),
HtmlPdfGenerator: require('./gen/html-pdf-generator'),
@ -13,5 +16,6 @@ module.exports = {
MarkdownGenerator: require('./gen/markdown-generator'),
JsonGenerator: require('./gen/json-generator'),
YamlGenerator: require('./gen/yaml-generator'),
JsonYamlGenerator: require('./gen/json-yaml-generator')
JsonYamlGenerator: require('./gen/json-yaml-generator'),
LaTeXGenerator: require('./gen/latex-generator')
};

View File

@ -29,7 +29,9 @@ Base resume generator for FluentCV.
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3
resumeNotFound: 3,
missingCommand: 4,
invalidCommand: 5
},
/**

View File

@ -1,32 +1,40 @@
/**
HTML resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
@license Copyright (c) 2015 James M. Devlin / FluentDesk
*/
var TemplateGenerator = require('./template-generator');
var FS = require('fs-extra');
var HTML = require( 'html' );
(function() {
var HtmlGenerator = module.exports = TemplateGenerator.extend({
var TemplateGenerator = require('./template-generator')
, FS = require('fs-extra')
, HTML = require( 'html' )
, PATH = require('path');
init: function() {
this._super( 'html' );
},
var HtmlGenerator = module.exports = TemplateGenerator.extend({
/**
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] };
});
init: function() {
this._super( 'html' );
},
return this.opts.prettify ?
HTML.prettyPrint( mk, this.opts.prettify ) : mk;
}
/**
Copy satellite CSS files to the destination and optionally pretty-print
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,74 +1,71 @@
/**
HTML-based PDF resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the HtmlPdfGenerator class.
@license Copyright (c) 2015 James M. Devlin / FluentDesk
*/
var TemplateGenerator = require('./template-generator');
var FS = require('fs-extra');
var HTML = require( 'html' );
(function() {
var HtmlPdfGenerator = module.exports = TemplateGenerator.extend({
init: function() {
this._super( 'pdf', 'html' );
},
var TemplateGenerator = require('./template-generator')
, FS = require('fs-extra')
, HTML = require( 'html' );
/**
Generate an HTML resume with optional pretty printing.
TODO: Avoid copying the CSS file to dest if we don't need to.
An HTML-based PDF resume generator for FluentCV.
*/
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;
var HtmlPdfGenerator = module.exports = TemplateGenerator.extend({
pdf(mk, outputFile);
return mk;
}
init: function() {
this._super( 'pdf', 'html' );
},
});
/**
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');
/**
Generate the binary PDF.
*/
onBeforeSave: function( info ) {
pdf(info.mk, info.outputFile);
return info.mk;
}
require('wkhtmltopdf')( markup, { pageSize: 'letter' } )
.pipe( FS.createWriteStream( fOut2 ) );
});
/**
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++;
}
}
}
}());

View File

@ -19,9 +19,9 @@ var JsonGenerator = module.exports = BaseGenerator.extend({
invoke: function( rez ) {
// TODO: merge with FCVD
function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index',
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview'],
'isModified', 'htmlPreview', 'safe' ],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}

View File

@ -0,0 +1,17 @@
/**
LaTeX resume generator for FluentCV.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
*/
var TemplateGenerator = require('./template-generator');
/**
LaTeXGenerator generates a LaTeX resume via TemplateGenerator.
*/
var LaTeXGenerator = module.exports = TemplateGenerator.extend({
init: function(){
this._super( 'latex', 'tex' );
}
});

View File

@ -1,173 +1,242 @@
/**
Template-based resume generator base for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
*/
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');
(function() {
// 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;
},
var FS = require( 'fs-extra' )
, _ = require( 'underscore' )
, MD = require( 'marked' )
, XML = require( 'xml-escape' )
, PATH = require('path')
, MKDIRP = require('mkdirp')
, BaseGenerator = require( './base-generator' )
, EXTEND = require('../utils/extend')
, Theme = require('../core/theme');
/** 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 options.
var _defaultOpts = {
engine: 'underscore',
keepBreaks: true,
freezeBreaks: false,
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(); },
link: function( name, url ) { return url ?
'<a href="' + url + '">' + name + '</a>' : name; }
},
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
unformatted: ['em','strong','a'],
max_char: 80, // ← See lib/html.js in above-linked repo
//wrap_line_length: 120, <-- Don't use this
}
};
/** Default generation method for template-based generators. */
generate: function( rez, f, opts ) {
// Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts );
/**
TemplateGenerator performs resume generation via local Handlebar or Underscore
style template expansion and is appropriate for text-based formats like HTML,
plain text, and XML versions of Microsoft Word, Excel, and OpenOffice.
@class TemplateGenerator
*/
var TemplateGenerator = module.exports = BaseGenerator.extend({
init: function( outputFormat, templateFormat, cssFile ){
this._super( outputFormat );
this.tplFormat = templateFormat || outputFormat;
},
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
@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 ) {
// Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts );
// Load the theme
var themeInfo = themeFromMoniker.call( this );
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 );
}
else if( tplInfo.action === null && theme.explicit ) {
var thisFilePath = PATH.join(outFolder, tplInfo.orgPath);
try {
MKDIRP.sync( PATH.dirname(thisFilePath) );
FS.copySync( tplInfo.path, thisFilePath );
}
catch(ex) {
console.log(ex);
}
}
});
// Some themes require a symlink structure. If so, create it.
if( curFmt.symLinks ) {
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)
FS.symlinkSync( absTarg, absLoc, type);
});
}
},
/**
Perform a single resume JSON-to-DEST resume transformation.
@param json A FRESH or JRS resume object.
@param jst The stringified template data
@param format The format name, such as "html" or "latex"
@param cssInfo Needs to be refactored.
@param opts Options and passthrough data.
*/
single: function( json, jst, format, cssInfo, opts ) {
this.opts.freezeBreaks && ( jst = freeze(jst) );
var eng = require( '../eng/' + opts.themeObj.engine + '-generator' );
var result = eng( json, jst, format, cssInfo, opts );
this.opts.freezeBreaks && ( result = unfreeze(result) );
return result;
}
});
/**
Export the TemplateGenerator function/ctor.
*/
module.exports = TemplateGenerator;
/**
Given a theme title, load the corresponding theme.
*/
function themeFromMoniker() {
// Verify the specified theme name/path
var tFolder = PATH.resolve( __dirname, this.opts.themeRelative, this.opts.theme );
var tFolder = PATH.join(
PATH.parse( require.resolve('fluent-themes') ).dir,
this.opts.theme
);
var exists = require('../utils/file-exists');
if (!exists( tFolder )) {
if( !exists( tFolder ) ) {
tFolder = PATH.resolve( this.opts.theme );
if (!exists( tFolder )) {
throw { fluenterror: this.codes.themeNotFound, data: 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 );
var t = this.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;
// Load the theme and format
return {
theme: t,
folder: tFolder
};
}
});
/**
Export the TemplateGenerator function/ctor.
*/
module.exports = TemplateGenerator;
/**
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 );
try {
MKDIRP.sync( PATH.dirname( tplInfo.major ? f : thisFilePath) );
FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } );
}
catch(ex) {
console.log(ex);
}
}
/**
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' )
};
/**
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' )
};
}());

View File

@ -1,14 +1,19 @@
#! /usr/bin/env node
/**
Command-line interface (CLI) for FluentCV via Node.js.
@license Copyright (c) 2015 | James M. Devlin
Command-line interface (CLI) for FluentCV:CLI.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
*/
var ARGS = require( 'minimist' )
, FCMD = require( './fluentcmd')
, PKG = require('../package.json')
, opts = { };
, COLORS = require('colors')
, FS = require('fs')
, PATH = require('path')
, opts = { }
, title = ('*** FluentCV v' + PKG.version + ' ***').bold.white
, _ = require('underscore');
@ -23,22 +28,50 @@ catch( ex ) {
function main() {
// Setup.
var title = '*** FluentCV v' + PKG.version + ' ***';
if( process.argv.length <= 2 ) { logMsg(title); throw { fluenterror: 3 }; }
var args = ARGS( process.argv.slice(2) );
opts = getOpts( args );
// Colorize
COLORS.setTheme({
title: ['white','bold'],
info: process.platform === 'win32' ? 'gray' : ['white','dim'],
infoBold: ['white','dim'],
warn: 'yellow',
error: 'red',
guide: 'yellow',
status: 'gray',//['white','dim'],
useful: 'green',
});
// Setup
if( process.argv.length <= 2 ) { throw { fluenterror: 4 }; }
var a = ARGS( process.argv.slice(2) );
opts = getOpts( a );
logMsg( title );
// Convert arguments to source files, target files, options
var src = args._ || [];
var dst = (args.o && ((typeof args.o === 'string' && [ args.o ]) || args.o)) || [];
dst = (dst === true) ? [] : dst; // Handle -o with missing output file
// Generate!
FCMD.generate( src, dst, opts, logMsg );
// Get the action to be performed
var params = a._.map( function(p){ return p.toLowerCase().trim(); });
var verb = params[0];
if( !FCMD.verbs[ verb ] ) {
logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn);
return;
}
// Get source and dest params
var splitAt = _.indexOf( params, 'to' );
if( splitAt === a._.length - 1 ) {
// 'TO' cannot be the last argument
logMsg('Please '.warn + 'specify an output file'.warn.bold +
' for this operation or '.warn + 'omit the TO keyword'.warn.bold +
'.'.warn );
return;
}
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 );
var parms = [ src, dst, opts, logMsg ];
// Invoke the action
FCMD.verbs[ verb ].apply( null, parms );
process.exit(0);
}
function logMsg( msg ) {
@ -46,10 +79,11 @@ function logMsg( msg ) {
}
function getOpts( args ) {
var noPretty = args['nopretty'] || args.n;
var noPretty = args.nopretty || args.n;
noPretty = noPretty && (noPretty === true || noPretty === 'true');
return {
theme: args.t || 'modern',
format: args.f || 'FRESH',
prettify: !noPretty,
silent: args.s || args.silent
};
@ -57,13 +91,27 @@ function getOpts( args ) {
function handleError( ex ) {
var msg = '', exitCode;
if( ex.fluenterror ){
switch( ex.fluenterror ) { // TODO: Remove magic numbers
case 1: msg = "The specified theme couldn't be found: " + ex.data; break;
case 2: msg = "Couldn't copy CSS file to destination folder"; break;
case 3: msg = "Please specify a valid JSON resume file."; break;
};
case 3: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in FRESH or JSON Resume format.'.guide; break;
case 4: msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide +
Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? 'or '.guide : '') +
v.toUpperCase().guide;
}).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold;
break;
//case 4: msg = title + '\n' + ; break;
case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break;
case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break;
case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break;
}
exitCode = ex.fluenterror;
}
else {
msg = ex.toString();
@ -72,7 +120,11 @@ function handleError( ex ) {
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
console.log( 'ERROR: ' + trimmed.toString() );
if( !ex.fluenterror || ex.fluenterror < 3 )
console.log( ('ERROR: ' + trimmed.toString()).red.bold );
else
console.log( trimmed.toString() );
process.exit( exitCode );
}

25
src/use.txt Normal file
View File

@ -0,0 +1,25 @@
Usage:
fluentcv <COMMAND> <SOURCES> [TO <TARGETS>] [-t <THEME>] [-f <FORMAT>]
<COMMAND> should be BUILD, NEW, CONVERT, VALIDATE, or HELP. <SOURCES> should
be the path to one or more FRESH or JSON Resume format resumes. <TARGETS>
should be the name of the destination resume to be created, if any. The
<THEME> parameter should be the name of a predefined theme (for example:
COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a custom
theme. <FORMAT> should be either FRESH (for a FRESH-format resume) or JRS
(for a JSON Resume-format resume).
fluentcv BUILD resume.json TO out/resume.all
fluentcv NEW resume.json
fluentcv CONVERT resume.json TO resume-jrs.json
fluentcv VALIDATE resume.json
Both SOURCES and TARGETS can accept multiple files:
fluentCV BUILD r1.json r2.json TO out/resume.all out2/resume.html
fluentCV NEW r1.json r2.json r3.json
fluentCV VALIDATE resume.json resume2.json resume3.json
See https://github.com/fluentdesk/fluentCV/blob/master/README.md for more
information.

View File

@ -41,7 +41,7 @@
return ret;
};
})(name, prop[name]) :
})(name, prop[name]) : // jshint ignore:line
prop[name];
}

View File

@ -0,0 +1,130 @@
{
"basics": {
"name": "Richard Hendriks",
"label": "Programmer",
"picture": "",
"email": "richard.hendriks@gmail.com",
"phone": "(912) 555-4321",
"website": "http://richardhendricks.com",
"summary": "Richard hails from Tulsa. He has earned degrees from the University of Oklahoma and Stanford. (Go Sooners and Cardinals!) Before starting Pied Piper, he worked for Hooli as a part time software developer. While his work focuses on applied information theory, mostly optimizing lossless compression schema of both the length-limited and adaptive variants, his non-work interests range widely, everything from quantum computing to chaos theory. He could tell you about it, but THAT would NOT be a “length-limited” conversation!",
"location": {
"address": "2712 Broadway St",
"postalCode": "CA 94115",
"city": "San Francisco",
"countryCode": "US",
"region": "California"
},
"profiles": [
{
"network": "Twitter",
"username": "neutralthoughts",
"url": ""
},
{
"network": "SoundCloud",
"username": "dandymusicnl",
"url": "https://soundcloud.com/dandymusicnl"
}
]
},
"work": [
{
"company": "Pied Piper",
"position": "CEO/President",
"website": "http://piedpiper.com",
"startDate": "2013-12-01",
"endDate": "2014-12-01",
"summary": "Pied Piper is a multi-platform technology based on a proprietary universal compression algorithm that has consistently fielded high Weisman Scores™ that are not merely competitive, but approach the theoretical limit of lossless compression.",
"highlights": [
"Build an algorithm for artist to detect if their music was violating copy right infringement laws",
"Successfully won Techcrunch Disrupt",
"Optimized an algorithm that holds the current world record for Weisman Scores"
]
}
],
"volunteer": [
{
"organization": "CoderDojo",
"position": "Teacher",
"website": "http://coderdojo.com/",
"startDate": "2012-01-01",
"endDate": "2013-01-01",
"summary": "Global movement of free coding clubs for young people.",
"highlights": [
"Awarded 'Teacher of the Month'"
]
}
],
"education": [
{
"institution": "University of Oklahoma",
"area": "Information Technology",
"studyType": "Bachelor",
"startDate": "2011-06-01",
"endDate": "2014-01-01",
"gpa": "4.0",
"courses": [
"DB1101 - Basic SQL",
"CS2011 - Java Introduction"
]
}
],
"awards": [
{
"title": "Digital Compression Pioneer Award",
"date": "2014-11-01",
"awarder": "Techcrunch",
"summary": "There is no spoon."
}
],
"publications": [
{
"name": "Video compression for 3d media",
"publisher": "Hooli",
"releaseDate": "2014-10-01",
"website": "http://en.wikipedia.org/wiki/Silicon_Valley_(TV_series)",
"summary": "Innovative middle-out compression algorithm that changes the way we store data."
}
],
"skills": [
{
"name": "Web Development",
"level": "Master",
"keywords": [
"HTML",
"CSS",
"Javascript"
]
},
{
"name": "Compression",
"level": "Master",
"keywords": [
"Mpeg",
"MP4",
"GIF"
]
}
],
"languages": [
{
"language": "English",
"fluency": "Native speaker"
}
],
"interests": [
{
"name": "Wildlife",
"keywords": [
"Ferrets",
"Unicorns"
]
}
],
"references": [
{
"name": "Erlich Bachman",
"reference": "It is my pleasure to recommend Richard, his performance working as a consultant for Main St. Company proved that he will be a valuable addition to any company."
}
]
}

36
tests/test-converter.js Normal file
View File

@ -0,0 +1,36 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, CONVERTER = require('../src/core/convert')
, FS = require('fs')
, _ = require('underscore');
chai.config.includeStack = false;
describe('FRESH/JRS converter', function () {
var _sheet;
it('should round-trip from JRS to FRESH to JRS without modifying or losing data', function () {
var fileA = path.join( __dirname, 'jrs-exemplar/richard-hendriks.json' );
var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' );
_sheet = new FRESHResume().open( fileA );
_sheet.saveAs( fileB, 'JRS' );
var rawA = FS.readFileSync( fileA, 'utf8' );
var rawB = FS.readFileSync( fileB, 'utf8' );
var objA = JSON.parse( rawA );
var objB = JSON.parse( rawB );
_.isEqual(objA, objB).should.equal(true);
});
});

73
tests/test-fresh-sheet.js Normal file
View File

@ -0,0 +1,73 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, validator = require('is-my-json-valid');
chai.config.includeStack = false;
describe('jane-doe.json (FRESH)', function () {
var _sheet;
it('should open without throwing an exception', function () {
function tryOpen() {
_sheet = new FRESHResume().open(
'node_modules/FRESCA/exemplar/jane-doe.json' );
}
tryOpen.should.not.Throw();
});
it('should have one or more of each section', function() {
expect(
//(_sheet.basics) &&
_sheet.name && _sheet.info && _sheet.location && _sheet.contact &&
(_sheet.employment.history && _sheet.employment.history.length > 0) &&
(_sheet.skills && _sheet.skills.list.length > 0) &&
(_sheet.education.history && _sheet.education.history.length > 0) &&
(_sheet.service.history && _sheet.service.history.length > 0) &&
(_sheet.writing && _sheet.writing.length > 0) &&
(_sheet.recognition && _sheet.recognition.length > 0) &&
(_sheet.samples && _sheet.samples.length > 0) &&
(_sheet.references && _sheet.references.length > 0) &&
(_sheet.interests && _sheet.interests.length > 0)
).to.equal( true );
});
it('should have a work duration of 7 years', function() {
_sheet.computed.numYears.should.equal( 7 );
});
it('should save without throwing an exception', function(){
function trySave() {
_sheet.save( 'tests/sandbox/jane-doe.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new FRESHResume().open('tests/sandbox/jane-doe.json');
_sheet.stringify().should.equal( savedSheet.stringify() )
});
it('should validate against the FRESH resume schema', function() {
var result = _sheet.isValid();
// var schemaJson = require('FRESCA');
// var validate = validator( schemaJson, { verbose: true } );
// var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n");
result.should.equal( true );
});
});
// describe('subtract', function () {
// it('should return -1 when passed the params (1, 2)', function () {
// expect(math.subtract(1, 2)).to.equal(-1);
// });
// });

View File

@ -4,18 +4,18 @@ var chai = require('chai')
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, Sheet = require('../src/core/sheet')
, JRSResume = require('../src/core/jrs-resume')
, validator = require('is-my-json-valid');
chai.config.includeStack = false;
describe('fullstack.json', function () {
describe('fullstack.json (JRS)', function () {
var _sheet;
it('should open without throwing an exception', function () {
function tryOpen() {
_sheet = new Sheet().open( 'node_modules/resample/fullstack/in/resume.json' );
_sheet = new JRSResume().open( 'node_modules/resample/fullstack/in/resume.json' );
}
tryOpen.should.not.Throw();
});
@ -44,14 +44,14 @@ describe('fullstack.json', function () {
});
it('should not be modified after saving', function() {
var savedSheet = new Sheet().open( 'tests/sandbox/fullstack.json' );
var savedSheet = new JRSResume().open( 'tests/sandbox/fullstack.json' );
_sheet.stringify().should.equal( savedSheet.stringify() )
});
it('should validate against the JSON Resume schema', function() {
var schemaJson = require('../src/core/resume.json');
var validate = validator( schemaJson, { verbose: true } );
var result = validate( JSON.parse( _sheet.meta.raw ) );
var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", validate.errors, "\n\n");
result.should.equal( true );

110
tests/test-themes.js Normal file
View File

@ -0,0 +1,110 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, FCMD = require( '../src/fluentcmd')
, validator = require('is-my-json-valid')
, COLORS = require('colors');
chai.config.includeStack = false;
describe('Testing themes', function () {
var _sheet;
COLORS.setTheme({
title: ['white','bold'],
info: process.platform === 'win32' ? 'gray' : ['white','dim'],
infoBold: ['white','dim'],
warn: 'yellow',
error: 'red',
guide: 'yellow',
status: 'gray',//['white','dim'],
useful: 'green',
});
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();
});
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();
});
});
// describe('subtract', function () {
// it('should return -1 when passed the params (1, 2)', function () {
// expect(math.subtract(1, 2)).to.equal(-1);
// });
// });