1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-11-05 09:56:22 +00:00

Merge pull request #75 from hacksalot/dev

Changes for v1.3.0-beta
This commit is contained in:
hacksalot 2015-12-30 21:26:27 -05:00
commit 6f37ccdee3
31 changed files with 1032 additions and 405 deletions

58
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,58 @@
Contributing
============
*Note: HackMyResume is also available as [FluentCV][fcv]. Contributors are
credited in both.*
HackMyResume needs your help! Our contribution workflow is based on [GitHub
Flow][flow] and we respond to all pull requests and issues, usually within 24
hours. HackMyResume has no corporate affiliation and no commercial basis, which
allows the project to maintain a strict user-first policy, rapid development
velocity, and a liberal stance on contributions and exotic functionality in
keeping with the spirit (and name) of the tool.
In short, your code is welcome here.
## How To Contribute
1. Optional: [**open an issue**][iss] identifying the feature or bug you'd like
to implement or fix. This step isn't required — you can start hacking away on
HackMyResume without clearing it with us — but helps avoid duplication of work
and ensures that your changes will be accepted once submitted.
2. **Fork and clone** the HackMyResume project.
3. Ideally, **create a new feature branch** (eg, `feat/new-awesome-feature` or
similar; call it whatever you like) to perform your work in.
4. **Install dependencies** by running `npm install` in the top-level
HackMyResume folder.
5. Make your **commits** as usual.
6. **Verify** your changes locally with `npm test`.
7. **Push** your commits.
7. **Submit a pull request** from your feature branch to the HackMyResume `dev`
branch.
8. We'll typically **respond** within 24 hours.
9. Your awesome changes will be **merged** after verification.
## Project Maintainers
HackMyResume is currently maintained by [hacksalot][ha] with assistance from
[tomheon][th] and our awesome [contributors][awesome]. Please direct all official
or internal inquiries to:
```
admin@hackmyresume.com
```
You can reach hacksalot directly at:
```
hacksalot@indevious.com
```
Thanks! See you out there in the trenches.
[fcv]: https://github.com/fluentdesk/fluentcv
[flow]: https://guides.github.com/introduction/flow/
[iss]: https://github.com/hacksalot/HackMyResume/issues
[ha]: https://github.com/hacksalot
[th]: https://github.com/tomheon
[awesome]: https://github.com/hacksalot/HackMyResume/graphs/contributors

View File

@ -14,10 +14,10 @@ module.exports = function (grunt) {
ui: 'bdd', ui: 'bdd',
reporter: 'spec' reporter: 'spec'
}, },
all: { src: ['tests/*.js'] } all: { src: ['test/*.js'] }
}, },
clean: ['tests/sandbox'], clean: ['test/sandbox'],
yuidoc: { yuidoc: {
compile: { compile: {
@ -38,7 +38,7 @@ module.exports = function (grunt) {
laxcomma: true, laxcomma: true,
expr: true expr: true
}, },
all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'] all: ['Gruntfile.js', 'src/**/*.js', 'test/*.js']
} }
}; };
@ -51,9 +51,9 @@ module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-clean');
grunt.registerTask('test', 'Test the HackMyResume library.', grunt.registerTask('test', 'Test the HackMyResume library.',
function( config ) { grunt.task.run( ['clean','simplemocha:all'] ); }); function( config ) { grunt.task.run( ['clean','jshint','simplemocha:all'] ); });
grunt.registerTask('document', 'Generate HackMyResume library documentation.', grunt.registerTask('document', 'Generate HackMyResume library documentation.',
function( config ) { grunt.task.run( ['yuidoc'] ); }); function( config ) { grunt.task.run( ['yuidoc'] ); });
grunt.registerTask('default', [ 'jshint', 'test', 'yuidoc' ]); grunt.registerTask('default', [ 'test', 'yuidoc' ]);
}; };

102
README.md
View File

@ -32,6 +32,7 @@ or Windows.
- Support for multiple input and output resumes. - Support for multiple input and output resumes.
- Use from your command line or [desktop][7]. - Use from your command line or [desktop][7].
- Free and open-source through the MIT license. - Free and open-source through the MIT license.
- Updated daily.
## Install ## Install
@ -42,8 +43,36 @@ Install HackMyResume with NPM:
``` ```
Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for 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 your platform.
access to `xelatex` and similar.
## Installing Themes
HackMyResume supports both [FRESH][fresh-themes] and [JSON Resume][jrst]-style
résumé themes.
- FRESH themes currently come preinstalled with HackMyResume.
- JSON Resume themes can be installed from NPM, GitHub, or manually.
To install a JSON Resume theme, just `cd` to the folder where you want to store
your themes and run one of:
```bash
# Install with NPM
npm install jsonresume-theme-[theme-name]
# Install with GitHub
git clone https://github.com/[user-or-org]/[repo-name]
```
Then when you're ready to generate your resume, just reference the location of
the theme folder as you installed it:
```bash
hackmyresume BUILD resume.json TO out/resume.all -t node_modules/jsonresume-theme-classy
```
Note: You can use install themes anywhere on your file system. You don't need a
package.json or other NPM/Node infrastructure.
## Getting Started ## Getting Started
@ -98,9 +127,9 @@ 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. 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. 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). LaTeX | .tex | A structured LaTeX document (or collection of documents) that can be processed with pdflatex, xelatex, and similar tools.
MS Word | .doc | A Microsoft Word office document. MS Word | .doc | A Microsoft Word office document (XML-driven; WordProcessingML).
Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme. Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme (through wkhtmltopdf).
plain text | .txt | A formatted plain text document appropriate for emails or copy-paste. plain text | .txt | A formatted plain text document appropriate for emails or copy-paste.
JSON | .json | A JSON representation of the resume. JSON | .json | A JSON representation of the resume.
YAML | .yml | A YAML representation of the resume. YAML | .yml | A YAML representation of the resume.
@ -108,15 +137,6 @@ RTF | .rtf | Forthcoming.
Textile | .textile | Forthcoming. Textile | .textile | Forthcoming.
image | .png, .bmp | Forthcoming. image | .png, .bmp | Forthcoming.
## Install
HackMyResume requires a recent version of [Node.js][4] and [NPM][5]. Then:
1. Install the latest official [wkhtmltopdf][3] binary for your platform.
2. Optionally install an updated LaTeX environment (LaTeX resumes only).
2. Install **HackMyResume** with `[sudo] npm install hackmyresume -g`.
3. You're ready to go.
## Use ## Use
Assuming you've got a JSON-formatted resume handy, generating resumes in Assuming you've got a JSON-formatted resume handy, generating resumes in
@ -132,19 +152,19 @@ theme (default to Modern). For example:
```bash ```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.) # Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.)
hackmyresume build resume.json -o out/resume.all -t modern hackmyresume BUILD resume.json TO out/resume.all -t modern
# Generate a specific resume format # Generate a specific resume format
hackmyresume build resume.json TO out/resume.html hackmyresume BUILD resume.json TO out/resume.html
hackmyresume build resume.json TO out/resume.pdf hackmyresume BUILD resume.json TO out/resume.pdf
hackmyresume build resume.json TO out/resume.md hackmyresume BUILD resume.json TO out/resume.md
hackmyresume build resume.json TO out/resume.doc hackmyresume BUILD resume.json TO out/resume.doc
hackmyresume build resume.json TO out/resume.json hackmyresume BUILD resume.json TO out/resume.json
hackmyresume build resume.json TO out/resume.txt hackmyresume BUILD resume.json TO out/resume.txt
hackmyresume build resume.json TO out/resume.yml hackmyresume BUILD resume.json TO out/resume.yml
# Specify 2 inputs and 3 outputs # Specify 2 inputs and 3 outputs
hackmyresume build in1.json in2.json TO out.html out.doc out.pdf hackmyresume BUILD in1.json in2.json TO out.html out.doc out.pdf
``` ```
You should see something to the effect of: You should see something to the effect of:
@ -171,8 +191,8 @@ For a predefined theme, include the theme name. For a custom theme, include the
path to the custom theme's folder. path to the custom theme's folder.
```bash ```bash
hackmyresume build resume.json -t modern hackmyresume BUILD resume.json TO out/rez.all -t modern
hackmyresume build resume.json -t ~/foo/bar/my-custom-theme/ hackmyresume BUILD resume.json TO OUT.rez.all -t ~/foo/bar/my-custom-theme/
``` ```
As of v1.0.0, available predefined themes are `positive`, `modern`, `compact`, As of v1.0.0, available predefined themes are `positive`, `modern`, `compact`,
@ -185,7 +205,7 @@ most generic to most specific:
```bash ```bash
# Merge specific.json onto base.json and generate all formats # Merge specific.json onto base.json and generate all formats
hackmyresume build base.json specific.json -o resume.all hackmyresume BUILD base.json specific.json TO resume.all
``` ```
This can be useful for overriding a base (generic) resume with information from This can be useful for overriding a base (generic) resume with information from
@ -196,7 +216,7 @@ resume. Merging follows conventional [extend()][9]-style behavior and there's
no arbitrary limit to how many resumes you can merge: no arbitrary limit to how many resumes you can merge:
```bash ```bash
hackmyresume build in1.json in2.json in3.json in4.json TO out.html out.doc hackmyresume BUILD in1.json in2.json in3.json in4.json TO out.html out.doc
Reading JSON resume: in1.json Reading JSON resume: in1.json
Reading JSON resume: in2.json Reading JSON resume: in2.json
Reading JSON resume: in3.json Reading JSON resume: in3.json
@ -212,14 +232,7 @@ You can specify **multiple output targets** and HackMyResume will build them:
```bash ```bash
# Generate out1.doc, out1.pdf, and foo.txt from me.json. # Generate out1.doc, out1.pdf, and foo.txt from me.json.
hackmyresume build me.json -o out1.doc -o out1.pdf -o foo.txt hackmyresume BUILD me.json TO out1.doc out1.pdf foo.txt
```
You can also omit the output file(s) and/or theme completely:
```bash
# Equivalent to "hackmyresume resume.json resume.all -t modern"
hackmyresume build resume.json
``` ```
### Using .all ### Using .all
@ -229,7 +242,7 @@ formats for the given resume. For example, this...
```bash ```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, etc.) # Generate all resume formats (HTML, PDF, DOC, TXT, etc.)
hackmyresume build me.json -o out/resume.all hackmyresume BUILD me.json TO out/resume.all
``` ```
..tells HackMyResume to read `me.json` and generate `out/resume.md`, ..tells HackMyResume to read `me.json` and generate `out/resume.md`,
@ -244,7 +257,7 @@ resumes, use the `validate` command:
```bash ```bash
# Validate myresume.json against either the FRESH or JSON Resume schema. # Validate myresume.json against either the FRESH or JSON Resume schema.
hackmyresume validate resumeA.json resumeB.json hackmyresume VALIDATE resumeA.json resumeB.json
``` ```
HackMyResume will validate each specified resume in turn: HackMyResume will validate each specified resume in turn:
@ -276,7 +289,7 @@ HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag
can be used: can be used:
```bash ```bash
hackmyresume generate resume.json out.all --nopretty hackmyresume BUILD resume.json out.all --nopretty
``` ```
### Silent Mode ### Silent Mode
@ -284,10 +297,16 @@ hackmyresume generate resume.json out.all --nopretty
Use `-s` or `--silent` to run in silent mode: Use `-s` or `--silent` to run in silent mode:
```bash ```bash
hackmyresume generate resume.json -o someFile.all -s hackmyresume BUILD resume.json -o someFile.all -s
hackmyresume generate resume.json -o someFile.all --silent hackmyresume BUILD resume.json -o someFile.all --silent
``` ```
## Contributing
HackMyResume is a community-driven free and open source project under the MIT
License. Contributions are encouraged and we respond to all PRs and issues,
usually within 24 hours. See [CONTRIBUTING.md][contribute] for details.
## License ## License
MIT. Go crazy. See [LICENSE.md][1] for details. MIT. Go crazy. See [LICENSE.md][1] for details.
@ -307,3 +326,6 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself [dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
[travis-image]: https://img.shields.io/travis/palomajs/paloma.svg?style=flat-square [travis-image]: https://img.shields.io/travis/palomajs/paloma.svg?style=flat-square
[travis-url]: https://travis-ci.org/hacksalot/HackMyResume [travis-url]: https://travis-ci.org/hacksalot/HackMyResume
[contribute]: CONTRIBUTING.md
[fresh-themes]: https://github.com/fluentdesk/fresh-themes
[jrst]: https://www.npmjs.com/search?q=jsonresume-theme

View File

@ -1,13 +1,13 @@
{ {
"name": "hackmyresume", "name": "hackmyresume",
"version": "1.2.2", "version": "1.3.0-beta",
"description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.", "description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/hacksalot/HackMyResume.git" "url": "https://github.com/hacksalot/HackMyResume.git"
}, },
"scripts": { "scripts": {
"test": "mocha && grunt", "test": "grunt clean && mocha",
"grunt": "grunt" "grunt": "grunt"
}, },
"keywords": [ "keywords": [
@ -29,6 +29,11 @@
"template" "template"
], ],
"author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)", "author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
"contributors": [
"Edmund Jorgensen (https://github.com/tomheon)",
"Ya Zhuang (https://github.com/zhuangya)",
"hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)"
],
"license": "MIT", "license": "MIT",
"preferGlobal": "true", "preferGlobal": "true",
"bugs": { "bugs": {
@ -41,7 +46,8 @@
"homepage": "https://github.com/hacksalot/HackMyResume", "homepage": "https://github.com/hacksalot/HackMyResume",
"dependencies": { "dependencies": {
"colors": "^1.1.2", "colors": "^1.1.2",
"fluent-themes": "~0.7.1-beta", "copy": "^0.1.3",
"fresh-themes": "~0.9.3-beta",
"fresca": "~0.2.2", "fresca": "~0.2.2",
"fs-extra": "^0.24.0", "fs-extra": "^0.24.0",
"handlebars": "^4.0.5", "handlebars": "^4.0.5",
@ -57,6 +63,7 @@
"path-exists": "^2.1.0", "path-exists": "^2.1.0",
"recursive-readdir-sync": "^1.0.6", "recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.2.0", "simple-html-tokenizer": "^0.2.0",
"string.prototype.startswith": "^0.2.0",
"underscore": "^1.8.3", "underscore": "^1.8.3",
"webshot": "^0.16.0", "webshot": "^0.16.0",
"wkhtmltopdf": "^0.1.5", "wkhtmltopdf": "^0.1.5",
@ -72,6 +79,11 @@
"grunt-contrib-yuidoc": "^0.10.0", "grunt-contrib-yuidoc": "^0.10.0",
"grunt-simple-mocha": "*", "grunt-simple-mocha": "*",
"jane-q-fullstacker": "fluentdesk/jane-q-fullstacker", "jane-q-fullstacker": "fluentdesk/jane-q-fullstacker",
"johnny-trouble-resume": "fluentdesk/johnny-trouble-resume",
"jsonresume-theme-boilerplate": "^0.1.2",
"jsonresume-theme-classy": "^1.0.9",
"jsonresume-theme-modern": "0.0.18",
"jsonresume-theme-sceptile": "^1.0.5",
"mocha": "*", "mocha": "*",
"resample": "fluentdesk/resample" "resample": "fluentdesk/resample"
} }

100
src/core/error-handler.js Normal file
View File

@ -0,0 +1,100 @@
/**
@module error-handler.js
*/
(function() {
var HACKMYSTATUS = require('./status-codes')
, PKG = require('../../package.json')
, FS = require('fs')
, FCMD = require('../hackmycmd')
, PATH = require('path')
, title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white;
var ErrorHandler = module.exports = {
err: function( ex, shouldExit ) {
var msg = '', exitCode;
if( ex.fluenterror ){
switch( ex.fluenterror ) { // TODO: Remove magic numbers
case HACKMYSTATUS.themeNotFound:
msg = "The specified theme couldn't be found: " + ex.data;
break;
case HACKMYSTATUS.copyCSS:
msg = "Couldn't copy CSS file to destination folder";
break;
case HACKMYSTATUS.resumeNotFound:
msg = 'Please '.guide + 'feed me a resume'.guide.bold +
' in FRESH or JSON Resume format.'.guide;
break;
case HACKMYSTATUS.missingCommand:
msg = title + "\nPlease ".guide + "give me a command".guide.bold +
" (".guide;
msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? 'or '.guide : '') +
v.toUpperCase().guide;
}).join(', '.guide) + ").\n\n".guide;
msg += FS.readFileSync(
PATH.resolve(__dirname, '../use.txt'), 'utf8' ).info.bold;
break;
case HACKMYSTATUS.invalidCommand:
msg = 'Please '.guide + 'specify the output resume file'.guide.bold +
' that should be created.'.guide;
break;
case HACKMYSTATUS.resumeNotFoundAlt:
msg = 'Please '.guide + 'feed me a resume'.guide.bold +
' in either FRESH or JSON Resume format.'.guide;
break;
case HACKMYSTATUS.inputOutputParity:
msg = 'Please '.guide + 'specify an output file name'.guide.bold +
' for every input file you wish to convert.'.guide;
break;
case HACKMYSTATUS.createNameMissing:
msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold +
' to create.'.guide;
break;
case HACKMYSTATUS.wkhtmltopdf:
msg = 'ERROR: PDF generation failed. '.red.bold + ('Make sure wkhtmltopdf is ' +
'installed and accessible from your path.').red;
break;
}
exitCode = ex.fluenterror;
}
else {
msg = ex.toString();
exitCode = 4;
}
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s
console.log( ('ERROR: ' + trimmed.toString()).red.bold );
console.log( ex.stack.gray);
}
else {
console.log( trimmed.toString() );
}
if( shouldExit )
process.exit( exitCode );
}
};
}());

View File

@ -143,15 +143,12 @@ Definition of the FRESHResume class.
}; };
/** /**
Open and parse the specified JSON resume sheet. Merge the JSON object model Initialize the FreshResume from JSON data.
onto this Sheet instance with extend() and convert sheet dates to a safe & Open and parse the specified FRESH resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending. consistent format. Then sort each section by startDate descending.
*/ */
FreshResume.prototype.parse = function( stringData, opts ) { FreshResume.prototype.parseJSON = function( rep, opts ) {
// Parse the incoming JSON representation
var rep = JSON.parse( stringData );
// Convert JSON Resume to FRESH if necessary // Convert JSON Resume to FRESH if necessary
if( rep.basics ) { if( rep.basics ) {
rep = CONVERTER.toFRESH( rep ); rep = CONVERTER.toFRESH( rep );
@ -178,6 +175,13 @@ Definition of the FRESHResume class.
return this; return this;
}; };
/**
Initialize the the FreshResume from string data.
*/
FreshResume.prototype.parse = function( stringData, opts ) {
return this.parseJSON( JSON.parse( stringData ), opts );
};
/** /**
Return a unique list of all keywords across all skills. Return a unique list of all keywords across all skills.
*/ */

View File

@ -1,37 +1,51 @@
/** /**
Definition of the Theme class. Definition of the FRESHTheme class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module fresh-theme.js
@module theme.js @license MIT. See LICENSE.md for details.
*/ */
(function() { (function() {
var FS = require('fs') var FS = require('fs')
, extend = require('../utils/extend') , extend = require('../utils/extend')
, validator = require('is-my-json-valid') , validator = require('is-my-json-valid')
, _ = require('underscore') , _ = require('underscore')
, PATH = require('path') , PATH = require('path')
, parsePath = require('parse-filepath') , parsePath = require('parse-filepath')
, pathExists = require('path-exists').sync
, EXTEND = require('../utils/extend') , EXTEND = require('../utils/extend')
, moment = require('moment') , moment = require('moment')
, RECURSIVE_READ_DIR = require('recursive-readdir-sync'); , READFILES = require('recursive-readdir-sync');
/** /**
The Theme class is a representation of a HackMyResume theme asset. The FRESHTheme class is a representation of a FRESH theme
@class Theme asset. See also: JRSTheme.
@class FRESHTheme
*/ */
function Theme() { function FRESHTheme() {
} }
/** /**
Open and parse the specified theme. Open and parse the specified theme.
*/ */
Theme.prototype.open = function( themeFolder ) { FRESHTheme.prototype.open = function( themeFolder ) {
this.folder = themeFolder;
// Open the [theme-name].json file; should have the same name as folder // Open the [theme-name].json file; should have the same name as folder
this.folder = themeFolder;
var pathInfo = parsePath( themeFolder ); var pathInfo = parsePath( themeFolder );
// Set up a formats hash for the theme
var formatsHash = { };
// Load the theme
var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' ); var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' );
var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) ); var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) );
var that = this; var that = this;
@ -39,9 +53,6 @@ Definition of the Theme class.
// Move properties from the theme JSON file to the theme object // Move properties from the theme JSON file to the theme object
EXTEND( true, this, themeInfo ); 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, // Check for an explicit "formats" entry in the theme JSON. If it has one,
// then this theme declares its files explicitly. // then this theme declares its files explicitly.
if( !!this.formats ) { if( !!this.formats ) {
@ -52,10 +63,6 @@ Definition of the Theme class.
formatsHash = loadImplicit.call( this ); formatsHash = loadImplicit.call( this );
} }
// 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 };
// Cache // Cache
this.formats = formatsHash; this.formats = formatsHash;
@ -65,20 +72,29 @@ Definition of the Theme class.
return this; return this;
}; };
/** /**
Determine if the theme supports the specified output format. Determine if the theme supports the specified output format.
*/ */
Theme.prototype.hasFormat = function( fmt ) { FRESHTheme.prototype.hasFormat = function( fmt ) {
return _.has( this.formats, fmt ); return _.has( this.formats, fmt );
}; };
/** /**
Determine if the theme supports the specified output format. Determine if the theme supports the specified output format.
*/ */
Theme.prototype.getFormat = function( fmt ) { FRESHTheme.prototype.getFormat = function( fmt ) {
return this.formats[ fmt ]; return this.formats[ fmt ];
}; };
/**
Load the theme implicitly, by scanning the theme folder for
files. TODO: Refactor duplicated code with loadExplicit.
*/
function loadImplicit() { function loadImplicit() {
// Set up a hash of formats supported by this theme. // Set up a hash of formats supported by this theme.
@ -92,7 +108,7 @@ Definition of the Theme class.
// Iterate over all files in the theme folder, producing an array, fmts, // 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 // containing info for each file. While we're doing that, also build up
// the formatsHash object. // the formatsHash object.
var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) { var fmts = READFILES(tplFolder).map( function(absPath) {
// If this file lives in a specific format folder within the theme, // If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format // such as "/latex" or "/html", then that format is the output format
@ -120,7 +136,7 @@ Definition of the Theme class.
// compact-[outputformat].[extension], for ex, compact-pdf.html. // compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) { if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-'); var idx = pathInfo.name.lastIndexOf('-');
outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 ); outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1);
isMajor = true; isMajor = true;
} }
@ -150,9 +166,13 @@ Definition of the Theme class.
}); });
// Now, get all the CSS files... // Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){ return fmt && (fmt.ext === 'css'); })) (this.cssFiles = fmts.filter(function( fmt ){
.forEach(function( cssf ) { return fmt && (fmt.ext === 'css');
}))
// For each CSS file, get its corresponding HTML file // For each CSS file, get its corresponding HTML file
.forEach(function( cssf ) {
var idx = _.findIndex(fmts, function( fmt ) { var idx = _.findIndex(fmts, function( fmt ) {
return fmt && fmt.pre === cssf.pre && fmt.ext === 'html'; return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
}); });
@ -169,30 +189,36 @@ Definition of the Theme class.
return formatsHash; return formatsHash;
} }
/**
Load the theme explicitly, by following the 'formats' hash
in the theme's JSON settings file.
*/
function loadExplicit() { function loadExplicit() {
var that = this; // Housekeeping
// Set up a hash of formats supported by this theme.
var formatsHash = { }; var formatsHash = { };
// Establish the base theme folder
var tplFolder = PATH.join( this.folder, 'src' ); var tplFolder = PATH.join( this.folder, 'src' );
var act = null; var act = null;
var that = this;
// Iterate over all files in the theme folder, producing an array, fmts, // 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 // containing info for each file. While we're doing that, also build up
// the formatsHash object. // the formatsHash object.
var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) { var fmts = READFILES( tplFolder ).map( function( absPath ) {
act = null; act = null;
// If this file is mentioned in the theme's JSON file under "transforms" // If this file is mentioned in the theme's JSON file under "transforms"
var pathInfo = parsePath(absPath); var pathInfo = parsePath(absPath);
var absPathSafe = absPath.trim().toLowerCase(); var absPathSafe = absPath.trim().toLowerCase();
var outFmt = _.find( Object.keys( that.formats ), function( fmtKey ) { var outFmt = _.find(
Object.keys( that.formats ),
function( fmtKey ) {
var fmtVal = that.formats[ fmtKey ]; var fmtVal = that.formats[ fmtKey ];
return _.some( fmtVal.transform, function( fpath ) { return _.some( fmtVal.transform, function(fpath) {
var absPathB = PATH.join( that.folder, fpath ).trim().toLowerCase(); var absPathB = PATH.join( that.folder, fpath )
.trim().toLowerCase();
return absPathB === absPathSafe; return absPathB === absPathSafe;
}); });
}); });
@ -216,7 +242,7 @@ Definition of the Theme class.
// compact-[outputformat].[extension], for ex, compact-pdf.html. // compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) { if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-'); var idx = pathInfo.name.lastIndexOf('-');
outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 ); outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1);
} }
// We should have a valid output format now. // We should have a valid output format now.
@ -246,7 +272,11 @@ Definition of the Theme class.
}); });
// Now, get all the CSS files... // Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; })) (this.cssFiles = fmts.filter(function( fmt ){
return fmt.ext === 'css';
}))
// For each CSS file, get its corresponding HTML file
.forEach(function( cssf ) { .forEach(function( cssf ) {
// For each CSS file, get its corresponding HTML file // For each CSS file, get its corresponding HTML file
var idx = _.findIndex(fmts, function( fmt ) { var idx = _.findIndex(fmts, function( fmt ) {
@ -264,12 +294,22 @@ Definition of the Theme class.
return formatsHash; return formatsHash;
} }
/**
Return a more friendly name for certain formats.
TODO: Refactor
*/
function friendlyName( val ) { function friendlyName( val ) {
val = val.trim().toLowerCase(); val = val.trim().toLowerCase();
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }; var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val; return friendly[val] || val;
} }
module.exports = Theme;
module.exports = FRESHTheme;
}()); }());

View File

@ -11,6 +11,7 @@ Definition of the JRSResume class.
, validator = require('is-my-json-valid') , validator = require('is-my-json-valid')
, _ = require('underscore') , _ = require('underscore')
, PATH = require('path') , PATH = require('path')
, MD = require('marked')
, moment = require('moment'); , moment = require('moment');
/** /**
@ -70,6 +71,7 @@ Definition of the JRSResume class.
}; };
/** /**
Initialize the JRS Resume from string data.
Open and parse the specified JSON resume sheet. Merge the JSON object model 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 & onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending. consistent format. Then sort each section by startDate descending.
@ -77,7 +79,14 @@ Definition of the JRSResume class.
JRSResume.prototype.parse = function( stringData, opts ) { JRSResume.prototype.parse = function( stringData, opts ) {
opts = opts || { }; opts = opts || { };
var rep = JSON.parse( stringData ); var rep = JSON.parse( stringData );
return this.parseJSON( rep, opts );
};
/**
Initialize the JRSRume from JSON data.
*/
JRSResume.prototype.parseJSON = function( rep, opts ) {
opts = opts || { };
extend( true, this, rep ); extend( true, this, rep );
// Set up metadata // Set up metadata
if( opts.imp === undefined || opts.imp ) { if( opts.imp === undefined || opts.imp ) {
@ -230,6 +239,70 @@ Definition of the JRSResume class.
}; };
JRSResume.prototype.dupe = function() {
var rnew = new JRSResume();
rnew.parse( this.stringify(), { } );
return rnew;
};
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
JRSResume.prototype.harden = function() {
var that = this;
var ret = this.dupe();
function HD(txt) {
return '@@@@~' + txt + '~@@@@';
}
function HDIN(txt){
//return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
return HD(txt);
}
// TODO: refactor recursion
function hardenStringsInObject( 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 ? HDIN(elem) : HD( elem );
else
hardenStringsInObject( 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','website','startDate','endDate','releaseDate','date','phone','email','address','postalCode','city','country','region'], key) )
return;
if( key === 'summary' )
obj[key] = HD( obj[key] );
else
obj[key] = inline ? HDIN( obj[key] ) : HD( obj[key] );
}
else
hardenStringsInObject( sub );
});
}
}
Object.keys( ret ).forEach(function(member){
hardenStringsInObject( ret[ member ] );
});
return ret;
};
/** /**
Convert human-friendly dates into formal Moment.js dates for all collections. 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 We don't want to lose the raw textual date as entered by the user, so we store

85
src/core/jrs-theme.js Normal file
View File

@ -0,0 +1,85 @@
/**
Definition of the JRSTheme class.
@module jrs-theme.js
@license MIT. See LICENSE.MD for details.
*/
(function() {
var _ = require('underscore')
, PATH = require('path')
, parsePath = require('parse-filepath')
, pathExists = require('path-exists').sync;
/**
The JRSTheme class is a representation of a JSON Resume
theme asset. See also: FRESHTheme.
@class JRSTheme
*/
function JRSTheme() {
}
/**
Open and parse the specified theme.
@method open
*/
JRSTheme.prototype.open = function( thFolder ) {
this.folder = thFolder;
// Open the [theme-name].json file; should have the same
// name as folder
var pathInfo = parsePath( thFolder );
// Open and parse the theme's package.json file.
var pkgJsonPath = PATH.join( thFolder, 'package.json' );
if( pathExists( pkgJsonPath )) {
var thApi = require( thFolder )
, thPkg = require( pkgJsonPath );
this.name = thPkg.name;
this.render = (thApi && thApi.render) || undefined;
this.formats = {
html: { title:'html', outFormat:'html', ext:'html' }
};
}
else {
throw { fluenterror: 10 };
}
return this;
};
/**
Determine if the theme supports the output format.
@method hasFormat
*/
JRSTheme.prototype.hasFormat = function( fmt ) {
return _.has( this.formats, fmt );
};
/**
Return the requested output format.
@method getFormat
*/
JRSTheme.prototype.getFormat = function( fmt ) {
return this.formats[ fmt ];
};
module.exports = JRSTheme;
}());

View File

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

View File

@ -0,0 +1,43 @@
/**
Core resume-loading logic for HackMyResume.
@module resume-factory.js
*/
(function(){
require('string.prototype.startswith');
var FS = require('fs');
var ResumeConverter = require('./convert');
/**
A simple factory class for FRESH and JSON Resumes.
@class ResumeFactory
*/
module.exports = {
/**
Load one or more resumes in a specific source format.
*/
load: function ( src, log, fn, toFormat ) {
toFormat = toFormat && (toFormat.toLowerCase().trim()) || 'fresh';
var ResumeClass = require('../core/' + toFormat + '-resume');
return src.map( function( res ) {
var rezJson = JSON.parse( FS.readFileSync( res ) );
var orgFormat = ( rezJson.meta && rezJson.meta.format &&
rezJson.meta.format.startsWith('FRESH@') ) ?
'fresh' : 'jrs';
if(orgFormat !== toFormat) {
rezJson = ResumeConverter[ 'to' + toFormat.toUpperCase() ]( rezJson );
}
// TODO: Core should not log
log( 'Reading '.info + orgFormat.toUpperCase().infoBold + ' resume: '.info + res.cyan.bold );
return (fn && fn(res)) || (new ResumeClass()).parseJSON( rezJson );
});
}
};
}());

22
src/core/spawn-watch.js Normal file
View File

@ -0,0 +1,22 @@
/**
@module spawn-watch.js
*/
(function() {
// Catch various out-of-band child process errors such as ENOENT for PDFs
// http://stackoverflow.com/q/27688804
var SpawnWatcher = module.exports = function() {
var childProcess = require("child_process");
var oldSpawn = childProcess.spawn;
childProcess.spawn = function() {
return oldSpawn.apply(this, arguments)
.on('error', function(err) {
require('./error-handler').err( err, false );
});
};
}();
//SpawnWatcher();
}());

23
src/core/status-codes.js Normal file
View File

@ -0,0 +1,23 @@
/**
Status codes for HackMyResume.
@module status-codes.js
@license MIT. See LICENSE.MD for details.
*/
(function(){
module.exports = {
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3,
missingCommand: 4,
invalidCommand: 5,
resumeNotFoundAlt: 6,
inputOutputParity: 7,
createNameMissing: 8,
wkhtmltopdf: 9,
missingPackageJSON: 10
};
}());

View File

@ -1,6 +1,6 @@
/** /**
Generic template helper definitions for FluentCV. Generic template helper definitions for HackMyResume / FluentCV.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
@module generic-helpers.js @module generic-helpers.js
*/ */
@ -114,6 +114,16 @@ Generic template helper definitions for FluentCV.
if (lhs || rhs) return options.fn(this); if (lhs || rhs) return options.fn(this);
}, },
/**
Conditional stylesheet link. Either display the link or embed the stylesheet
via <style></style> tag.
*/
styleSheet: function( file, options ) {
return ( this.opts.css === 'link') ?
'<link href="' + file + '" rel="stylesheet" type="text/css">' :
'<style>' + this.cssInfo.data + '</style>';
},
/** /**
Perform a generic comparison. Perform a generic comparison.
See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates

View File

@ -40,6 +40,7 @@ Definition of the HandlebarsGenerator class.
RAW: json, RAW: json,
filt: opts.filters, filt: opts.filters,
cssInfo: cssInfo, cssInfo: cssInfo,
opts: opts,
headFragment: opts.headFragment || '' headFragment: opts.headFragment || ''
}); });

View File

@ -11,7 +11,6 @@ Definition of the UnderscoreGenerator class.
var _ = require('underscore'); var _ = require('underscore');
/** /**
Perform template-based resume generation using Underscore.js. Perform template-based resume generation using Underscore.js.
@class UnderscoreGenerator @class UnderscoreGenerator
@ -32,6 +31,10 @@ Definition of the UnderscoreGenerator class.
// Strip {# comments #} // Strip {# comments #}
jst = jst.replace( delims.comment, ''); jst = jst.replace( delims.comment, '');
var helpers = require('./generic-helpers');
helpers.opts = opts;
helpers.cssInfo = cssInfo;
// Compile and run the template. TODO: avoid unnecessary recompiles. // Compile and run the template. TODO: avoid unnecessary recompiles.
var compiled = _.template(jst); var compiled = _.template(jst);
var ret = compiled({ var ret = compiled({
@ -40,7 +43,9 @@ Definition of the UnderscoreGenerator class.
XML: require('xml-escape'), XML: require('xml-escape'),
RAW: json, RAW: json,
cssInfo: cssInfo, cssInfo: cssInfo,
headFragment: opts.headFragment || '' headFragment: opts.headFragment || '',
opts: opts,
h: helpers
}); });
return ret; return ret;
} }

View File

@ -1,7 +1,7 @@
/** /**
Definition of the BaseGenerator class. Definition of the BaseGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module base-generator.js @module base-generator.js
@license MIT. See LICENSE.md for details.
*/ */
(function() { (function() {
@ -26,14 +26,7 @@ Definition of the BaseGenerator class.
/** /**
Status codes. Status codes.
*/ */
codes: { codes: require('../core/status-codes'),
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3,
missingCommand: 4,
invalidCommand: 5
},
/** /**
Generator options. Generator options.

View File

@ -1,6 +1,5 @@
/** /**
Definition of the HtmlPdfGenerator class. Definition of the HtmlPdfGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module html-pdf-generator.js @module html-pdf-generator.js
*/ */
@ -23,7 +22,7 @@ Definition of the HtmlPdfGenerator class.
Generate the binary PDF. Generate the binary PDF.
*/ */
onBeforeSave: function( info ) { onBeforeSave: function( info ) {
pdf( info.mk, info.outputFile ); pdf.call( this, info.mk, info.outputFile );
return null; // halt further processing return null; // halt further processing
} }
@ -34,39 +33,50 @@ Definition of the HtmlPdfGenerator class.
*/ */
function pdf( markup, fOut ) { function pdf( markup, fOut ) {
var pdfCount = 0; pdf_wkhtmltopdf.call( this, markup, fOut );
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 ) { Generate a PDF from HTML using wkhtmltopdf.
fOut2 = fOut2.replace(/\.pdf$/g, '.b.pdf'); */
function pdf_wkhtmltopdf( markup, fOut ) {
var wk;
try {
wk = require('wkhtmltopdf');
wk( markup, { pageSize: 'letter' } )
.pipe( FS.createWriteStream( fOut ) );
} }
require('wkhtmltopdf')( markup, { pageSize: 'letter' } ) catch(ex) {
.pipe( FS.createWriteStream( fOut2 ) ); // { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', syscall: 'write' }
pdfCount++; // { [Error: ENOENT] }
throw { fluenterror: this.codes.wkhtmltopdf };
} }
} }
// function pdf_phantom() {
// pdfCount++;
// require('phantom').create( function( ph ) {
// ph.createPage( function( page ) {
// page.setContent( markup );
// page.set('paperSize', {
// format: 'A4',
// orientation: 'portrait',
// margin: '1cm'
// });
// page.set("viewportSize", {
// width: 1024, // TODO: option-ify
// height: 768 // TODO: Use "A" sizes
// });
// page.set('onLoadFinished', function(success) {
// page.render( fOut );
// pdfCount++;
// ph.exit();
// });
// },
// { dnodeOpts: { weak: false } } );
// });
// }
}()); }());

View File

@ -1,6 +1,6 @@
/** /**
Definition of the HtmlPngGenerator class. Definition of the HtmlPngGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.MD for details.
@module html-png-generator.js @module html-png-generator.js
*/ */
@ -11,7 +11,7 @@ Definition of the HtmlPngGenerator class.
, HTML = require( 'html' ); , HTML = require( 'html' );
/** /**
An HTML-based PDF resume generator for HackMyResume. An HTML-based PNG resume generator for HackMyResume.
*/ */
var HtmlPngGenerator = module.exports = TemplateGenerator.extend({ var HtmlPngGenerator = module.exports = TemplateGenerator.extend({
@ -19,24 +19,29 @@ Definition of the HtmlPngGenerator class.
this._super( 'png', 'html' ); this._super( 'png', 'html' );
}, },
/** invoke: function( rez, themeMarkup, cssInfo, opts ) {
Generate the binary PDF. //return YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 );
*/ },
onBeforeSave: function( info ) {
png( info.mk, info.outputFile ); generate: function( rez, f, opts ) {
return null; // halt further processing var htmlResults = opts.targets.filter(function(t){
return t.fmt.outFormat === 'html';
});
var htmlFile = htmlResults[0].final.files.filter(function(fl){
return fl.info.ext === 'html';
});
png(htmlFile[0].data, f);
} }
}); });
/** /**
Generate a PDF from HTML. Generate a PNG from HTML.
*/ */
function png( markup, fOut ) { function png( markup, fOut ) {
// require('webshot')( markup , { encoding: 'binary', siteType: 'html' } )
require('webshot')( markup , { encoding: 'binary', siteType: 'html' } ) // .pipe( FS.createWriteStream( fOut ) );
.pipe( FS.createWriteStream( fOut ) ); require('webshot')( markup , fOut, { siteType: 'html' }, function(err) { } );
} }
}()); }());

View File

@ -1,6 +1,6 @@
/** /**
Definition of the TemplateGenerator class. Definition of the TemplateGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
@module template-generator.js @module template-generator.js
*/ */
@ -17,7 +17,8 @@ Definition of the TemplateGenerator class.
, MKDIRP = require('mkdirp') , MKDIRP = require('mkdirp')
, BaseGenerator = require( './base-generator' ) , BaseGenerator = require( './base-generator' )
, EXTEND = require('../utils/extend') , EXTEND = require('../utils/extend')
, Theme = require('../core/theme'); , FRESHTheme = require('../core/fresh-theme')
, JRSTheme = require('../core/jrs-theme');
@ -77,7 +78,14 @@ Definition of the TemplateGenerator class.
@method invoke @method invoke
@param rez A FreshResume object. @param rez A FreshResume object.
@param opts Generator options. @param opts Generator options.
@returns An array of strings representing generated output files. @returns An array of objects representing the generated output files. Each
object has this format:
{
files: [ { info: { }, data: [ ] }, { ... } ],
themeInfo: { }
}
*/ */
invoke: function( rez, opts ) { invoke: function( rez, opts ) {
@ -156,7 +164,7 @@ Definition of the TemplateGenerator class.
{ outputFile: fileName, mk: file.data } ); { outputFile: fileName, mk: file.data } );
} }
catch(ex) { catch(ex) {
console.log(ex); require('../core/error-handler').err(ex, false);
} }
} }
else if( file.info.action === null/* && theme.explicit*/ ) { else if( file.info.action === null/* && theme.explicit*/ ) {
@ -182,6 +190,8 @@ Definition of the TemplateGenerator class.
}); });
} }
return genInfo;
}, },
@ -220,11 +230,19 @@ Definition of the TemplateGenerator class.
Given a theme title, load the corresponding theme. Given a theme title, load the corresponding theme.
*/ */
function themeFromMoniker() { function themeFromMoniker() {
// Verify the specified theme name/path // Verify the specified theme name/path
var tFolder = PATH.join( var tFolder = PATH.join(
parsePath( require.resolve('fluent-themes') ).dirname, parsePath( require.resolve('fresh-themes') ).dirname,
this.opts.theme this.opts.theme
); );
var t;
if( this.opts.theme.startsWith('jsonresume-theme-') ) {
console.log('LOADING JSON RESUME');
t = new JRSTheme().open( tFolder );
}
else {
var exists = require('path-exists').sync; var exists = require('path-exists').sync;
if( !exists( tFolder ) ) { if( !exists( tFolder ) ) {
tFolder = PATH.resolve( this.opts.theme ); tFolder = PATH.resolve( this.opts.theme );
@ -232,8 +250,8 @@ Definition of the TemplateGenerator class.
throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme}; throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme};
} }
} }
t = this.opts.themeObj || new FRESHTheme().open( tFolder );
var t = this.opts.themeObj || new Theme().open( tFolder ); }
// Load the theme and format // Load the theme and format
return { return {

View File

@ -8,7 +8,8 @@ module.exports = {
Sheet: require('./core/fresh-resume'), Sheet: require('./core/fresh-resume'),
FRESHResume: require('./core/fresh-resume'), FRESHResume: require('./core/fresh-resume'),
JRSResume: require('./core/jrs-resume'), JRSResume: require('./core/jrs-resume'),
Theme: require('./core/theme'), FRESHTheme: require('./core/fresh-theme'),
JRSTheme: require('./core/jrs-theme'),
FluentDate: require('./core/fluent-date'), FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./gen/html-generator'), HtmlGenerator: require('./gen/html-generator'),
TextGenerator: require('./gen/text-generator'), TextGenerator: require('./gen/text-generator'),

View File

@ -8,29 +8,34 @@ Internal resume generation logic for HackMyResume.
module.exports = function () { module.exports = function () {
var unused = require('./utils/string') var unused = require('./utils/string')
, PATH = require('path'); , PATH = require('path')
, FS = require('fs');
/** /**
Display help documentation. Display help documentation.
*/ */
function help() { function help() {
console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ) var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' );
.useful.bold ); console.log( manPage.useful.bold );
} }
/** /**
Internal module interface. Used by FCV Desktop and HMR. Internal module interface. Used by FCV Desktop and HMR.
*/ */
return { var v = {
verbs: {
generate: require('./verbs/generate'),
build: require('./verbs/generate'), build: require('./verbs/generate'),
validate: require('./verbs/validate'), validate: require('./verbs/validate'),
convert: require('./verbs/convert'), convert: require('./verbs/convert'),
create: require('./verbs/create'),
new: require('./verbs/create'), new: require('./verbs/create'),
help: help help: help
};
return {
verbs: v,
alias: {
generate: v.build,
create: v.build
}, },
lib: require('./hackmyapi'), lib: require('./hackmyapi'),
options: require('./core/default-options'), options: require('./core/default-options'),

View File

@ -2,27 +2,32 @@
/** /**
Command-line interface (CLI) for HackMyResume. Command-line interface (CLI) for HackMyResume.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk. @license MIT. Copyright (c) 2015 hacksalot (https://github.com/hacksalot)
@module index.js @module index.js
*/ */
var ARGS = require( 'minimist' )
var SPAWNW = require('./core/spawn-watch')
, ARGS = require( 'minimist' )
, FCMD = require( './hackmycmd') , FCMD = require( './hackmycmd')
, PKG = require('../package.json') , PKG = require('../package.json')
, COLORS = require('colors') , COLORS = require('colors')
, FS = require('fs') , FS = require('fs')
, PATH = require('path') , PATH = require('path')
, HACKMYSTATUS = require('./core/status-codes')
, opts = { } , opts = { }
, title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white , title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white
, _ = require('underscore'); , _ = require('underscore');
try { try {
main(); main();
} }
catch( ex ) { catch( ex ) {
handleError( ex ); require('./core/error-handler').err( ex, true );
} }
@ -50,7 +55,7 @@ function main() {
// Get the action to be performed // Get the action to be performed
var params = a._.map( function(p){ return p.toLowerCase().trim(); }); var params = a._.map( function(p){ return p.toLowerCase().trim(); });
var verb = params[0]; var verb = params[0];
if( !FCMD.verbs[ verb ] ) { if( !FCMD.verbs[ verb ] && !FCMD.alias[ verb ] ) {
logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn); logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn);
return; return;
} }
@ -69,10 +74,9 @@ function main() {
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt ); var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 ); var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 );
( splitAt === -1 ) && src.length > 1 && dst.push( src.pop() ); // Allow omitting TO keyword ( splitAt === -1 ) && src.length > 1 && dst.push( src.pop() ); // Allow omitting TO keyword
var parms = [ src, dst, opts, logMsg ];
// Invoke the action // Invoke the action
FCMD.verbs[ verb ].apply( null, parms ); (FCMD.verbs[verb] || FCMD.alias[verb]).apply(null, [src, dst, opts, logMsg]);
} }
@ -87,50 +91,7 @@ function getOpts( args ) {
theme: args.t || 'modern', theme: args.t || 'modern',
format: args.f || 'FRESH', format: args.f || 'FRESH',
prettify: !noPretty, prettify: !noPretty,
silent: args.s || args.silent silent: args.s || args.silent,
css: args.css || 'embed'
}; };
} }
// TODO: refactor
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 '.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.'.guide; break;
case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break;
case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break;
case 8: msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold + ' to create.'.guide; break;
}
exitCode = ex.fluenterror;
}
else {
msg = ex.toString();
exitCode = 4;
}
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s
console.log( ('ERROR: ' + trimmed.toString()).red.bold );
console.log( ex.stack.gray);
}
else
console.log( trimmed.toString() );
process.exit( exitCode );
}

View File

@ -1,6 +1,5 @@
/** /**
Definitions of string utility functions. Definitions of string utility functions.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module string.js @module string.js
*/ */
@ -10,14 +9,18 @@ See: http://stackoverflow.com/a/32800728/4942583
@method isNullOrWhitespace @method isNullOrWhitespace
*/ */
String.isNullOrWhitespace = function( input ) { (function() {
String.isNullOrWhitespace = function( input ) {
return !input || !input.trim(); return !input || !input.trim();
}; };
String.prototype.endsWith = function(suffix) { String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1; return this.indexOf(suffix, this.length - suffix.length) !== -1;
}; };
String.is = function( val ) { String.is = function( val ) {
return typeof val === 'string' || val instanceof String; return typeof val === 'string' || val instanceof String;
}; };
}());

View File

@ -1,6 +1,6 @@
(function(){ (function(){
var loadSourceResumes = require('../core/load-source-resumes'); var ResumeFactory = require('../core/resume-factory');
/** /**
Convert between FRESH and JRS formats. Convert between FRESH and JRS formats.
@ -16,7 +16,7 @@
if( src && dst && src.length && dst.length && src.length !== dst.length ) { if( src && dst && src.length && dst.length && src.length !== dst.length ) {
throw { fluenterror: 7 }; throw { fluenterror: 7 };
} }
var sheets = loadSourceResumes( src, _log ); var sheets = ResumeFactory.load( src, _log );
sheets.forEach(function(sheet, idx){ sheets.forEach(function(sheet, idx){
var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH';
var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS';

View File

@ -1,15 +1,29 @@
/**
Implementation of the 'generate' verb for HackMyResume.
@module generate.js
@license MIT. See LICENSE.md for details.
*/
(function() { (function() {
var PATH = require('path') var PATH = require('path')
, parsePath = require('parse-filepath') , FS = require('fs')
, MD = require('marked')
, MKDIRP = require('mkdirp') , MKDIRP = require('mkdirp')
, EXTEND = require('../utils/extend')
, parsePath = require('parse-filepath')
, _opts = require('../core/default-options') , _opts = require('../core/default-options')
, FluentTheme = require('../core/theme') , FluentTheme = require('../core/fresh-theme')
, loadSourceResumes = require('../core/load-source-resumes') , JRSTheme = require('../core/jrs-theme')
, ResumeFactory = require('../core/resume-factory')
, _ = require('underscore') , _ = require('underscore')
, _fmts = require('../core/default-formats') , _fmts = require('../core/default-formats')
, _err, _log, rez; , _err, _log, rez;
/** /**
Handle an exception. Handle an exception.
*/ */
@ -17,28 +31,34 @@
throw ex; throw ex;
} }
module.exports =
/** /**
Given a source JSON resume, a destination resume path, and a theme file, Given a source resume in FRESH or JRS format, a destination resume path, and a
generate 0..N resumes in the desired formats. theme file, generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json". @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 dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern". @param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override. @param logger Optional logging override.
*/ */
function generate( src, dst, opts, logger, errHandler ) { function build( src, dst, opts, logger, errHandler ) {
// Housekeeping...
_log = logger || console.log; _log = logger || console.log;
_err = errHandler || error; _err = errHandler || error;
//_opts = extend( true, _opts, opts ); //_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false; _opts.prettify = opts.prettify === true ? _opts.prettify : false;
_opts.css = opts.css;
// Load the theme...
var tFolder = verify_theme( _opts.theme );
var theTheme = load_theme( tFolder );
// Load input resumes... // Load input resumes...
if( !src || !src.length ) { throw { fluenterror: 3 }; } if( !src || !src.length ) { throw { fluenterror: 3 }; }
var sheets = loadSourceResumes( src, _log ); var sheets = ResumeFactory.load( src, _log, null,
theTheme.render ? 'JRS' : 'FRESH' );
// Merge input resumes... // Merge input resumes...
var msg = ''; var msg = '';
@ -49,97 +69,100 @@
}); });
msg && _log(msg); msg && _log(msg);
// Verify the specified theme name/path // Expand output resumes...
var relativeThemeFolder = '../../node_modules/fluent-themes/themes'; var targets = expand( dst, theTheme );
var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme);
var exists = require('path-exists').sync;
if (!exists( tFolder )) {
tFolder = PATH.resolve( _opts.theme );
if (!exists( tFolder )) {
throw { fluenterror: 1, data: _opts.theme };
}
}
// Load the theme
var theTheme = (new FluentTheme()).open( tFolder );
_opts.themeObj = theTheme;
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
(' theme (' + Object.keys(theTheme.formats).length + ' formats)').info);
// Expand output resumes... (can't use map() here)
var targets = [], that = this;
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) {
var to = PATH.resolve(t),
pa = parsePath(to),
fmat = pa.extname || '.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! // Run the transformation!
var finished = targets.map( function(t) { return single(t, theTheme); }); targets.forEach( function(t) {
t.final = single( t, theTheme, targets );
});
// Don't send the client back empty-handed // Don't send the client back empty-handed
return { sheet: rez, targets: targets, processed: finished }; return { sheet: rez, targets: targets, processed: targets };
}; }
/** /**
Generate a single resume of a specific format. Generate a single target resume such as "out/rez.html" or "out/rez.doc".
@param f Full path to the destination resume to generate, for example, @param targInfo Information for the target resume.
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt". @param theme A FRESHTheme or JRSTheme object.
@returns
*/ */
function single( targInfo, theme ) { function single( targInfo, theme, finished ) {
function MDIN(txt) { // TODO: Move this
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
try { try {
var f = targInfo.file var f = targInfo.file
, fType = targInfo.fmt.outFormat , fType = targInfo.fmt.outFormat
, fName = PATH.basename(f, '.' + fType) , fName = PATH.basename(f, '.' + fType)
, theFormat; , theFormat;
// If targInfo.fmt.files exists, this theme has an explicit "files" _log( 'Generating '.useful +
// section in its theme.json file. targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + PATH.relative(process.cwd(), f ).useful.bold );
// If targInfo.fmt.files exists, this format is backed by a document.
// Fluent/FRESH themes are handled here.
if( targInfo.fmt.files && targInfo.fmt.files.length ) { if( targInfo.fmt.files && targInfo.fmt.files.length ) {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
theFormat = _fmts.filter( theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts ); _opts.targets = finished;
return theFormat.gen.generate( rez, f, _opts );
// targInfo.fmt.files.forEach( function( form ) {
//
// if( form.action === 'transform' ) {
// var theFormat = _fmts.filter( function( fmt ) {
// return fmt.name === targInfo.fmt.outFormat;
// })[0];
// MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
// theFormat.gen.generate( rez, f, _opts );
// }
// else if( form.action === null ) {
// // Copy the file
// }
//
// });
} }
// Otherwise the theme has no files section
else {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
theFormat = _fmts.filter( // Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; // (JSON, YML, or PNG) that every theme gets "for free".
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; else {
theFormat.gen.generate( rez, f, _opts );
theFormat = _fmts.filter( function(fmt) {
return fmt.name === targInfo.fmt.outFormat;
})[0];
var outFolder = PATH.dirname( f );
MKDIRP.sync( outFolder ); // Ensure dest folder exists;
// JSON Resume themes have a 'render' method that needs to be called
if( theme.render ) {
var COPY = require('copy');
var globs = [ /*'**',*/ '*.css', '*.js', '*.png', '*.jpg', '*.gif', '*.bmp' ];
COPY.sync( globs , outFolder, {
cwd: theme.folder, nodir: true,
ignore: ['node_modules/','node_modules/**']
// rewrite: function(p1, p2) {
// return PATH.join(p2, p1);
// }
});
// Prevent JSON Resume theme .js from chattering (TODO: redirect IO)
var consoleLog = console.log;
console.log = function() { };
// Call the theme's render method
var rezDupe = rez.harden();
var rezHtml = theme.render( rezDupe );
// Turn logging back on
console.log = consoleLog;
// Unharden
rezHtml = rezHtml.replace( /@@@@~.+?~@@@@/g, function(val){
return MDIN( val.replace( /~@@@@/gm,'' ).replace( /@@@@~/gm,'' ) );
});
// Save the file
FS.writeFileSync( f, rezHtml );
// Return markup to the client
return rezHtml;
}
else {
return theFormat.gen.generate( rez, f, _opts );
}
} }
} }
catch( ex ) { catch( ex ) {
@ -147,4 +170,117 @@
} }
} }
/**
Expand output files. For example, "foo.all" should be expanded to
["foo.html", "foo.doc", "foo.pdf", "etc"].
@param dst An array of output files as specified by the user.
@param theTheme A FRESHTheme or JRSTheme object.
*/
function expand( dst, theTheme ) {
// Add freebie formats (JSON, YAML, PNG) every theme gets...
// Add HTML-driven PNG only if the theme has an HTML format.
theTheme.formats.json = theTheme.formats.json || {
freebie: true, title: 'json', outFormat: 'json', pre: 'json',
ext: 'json', path: null, data: null
};
theTheme.formats.yml = theTheme.formats.yml || {
freebie: true, title: 'yaml', outFormat: 'yml', pre: 'yml',
ext: 'yml', path: null, data: null
};
if( theTheme.formats.html && !theTheme.formats.png ) {
theTheme.formats.png = {
freebie: true, title: 'png', outFormat: 'png',
ext: 'yml', path: null, data: null
};
}
// Set up the destination collection. It's either the array of files passed
// by the user or 'out/resume.all' if no targets were specified.
var destColl = (dst && dst.length && dst) ||
[PATH.normalize('out/resume.all')];
// Assemble an array of expanded target files... (can't use map() here)
var targets = [];
destColl.forEach( function(t) {
var to = PATH.resolve(t), pa = parsePath(to),fmat = pa.extname || '.all';
var explicitFormats = _.omit( theTheme.formats, function(val, key) {
return !val.freebie;
});
var implicitFormats = _.omit( theTheme.formats, function(val) {
return val.freebie;
});
targets.push.apply(
targets, fmat === '.all' ?
Object.keys( implicitFormats ).map( function( k ) {
var z = theTheme.formats[k];
return { file: to.replace( /all$/g, z.outFormat ), fmt: z };
}) :
[{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
targets.push.apply(
targets, fmat === '.all' ?
Object.keys( explicitFormats ).map( function( k ) {
var z = theTheme.formats[k];
return { file: to.replace( /all$/g, z.outFormat ), fmt: z };
}) :
[{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
});
return targets;
}
/**
Verify the specified theme name/path.
*/
function verify_theme( themeNameOrPath ) {
var tFolder = PATH.resolve(
__dirname,
'../../node_modules/fresh-themes/themes',
themeNameOrPath
);
var exists = require('path-exists').sync;
if( !exists( tFolder ) ) {
tFolder = PATH.resolve( themeNameOrPath );
if( !exists( tFolder ) ) {
throw { fluenterror: 1, data: _opts.theme };
}
}
return tFolder;
}
/**
Load the specified theme.
*/
function load_theme( tFolder ) {
// Create a FRESH or JRS theme object
var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ?
new JRSTheme().open(tFolder) : new FluentTheme().open( tFolder );
// Cache the theme object
_opts.themeObj = theTheme;
// Output a message TODO: core should not log
var numFormats = Object.keys(theTheme.formats).length;
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
(' theme (' + numFormats + ' formats)').info);
return theTheme;
}
module.exports = build;
}()); }());

View File

@ -1,7 +1,7 @@
(function() { (function() {
var FS = require('fs'); var FS = require('fs');
var loadSourceResumes = require('../core/load-source-resumes'); var ResumeFactory = require('../core/resume-factory');
module.exports = module.exports =
@ -20,7 +20,7 @@
}; };
// Load input resumes... // Load input resumes...
var sheets = loadSourceResumes(src, _log, function( res ) { var sheets = ResumeFactory.load(src, _log, function( res ) {
try { try {
return { return {
file: res, file: res,

View File

@ -43,15 +43,15 @@ describe('Testing CLI interface', function () {
silent: true silent: true
}; };
run( 'new', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' ); run( 'new', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' ); run( 'new', ['test/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
run( 'new', ['tests/sandbox/new-1.json', 'tests/sandbox/new-2.json', 'tests/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' ); run( 'new', ['test/sandbox/new-1.json', 'test/sandbox/new-2.json', 'test/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' );
run( 'new', ['tests/sandbox/new-jrs-1.json', 'tests/sandbox/new-jrs-2.json', 'tests/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' ); run( 'new', ['test/sandbox/new-jrs-1.json', 'test/sandbox/new-jrs-2.json', 'test/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' );
run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' ); run( 'new', ['test/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
fail( 'new', [], [], opts, " (when a filename isn't specified)" ); fail( 'new', [], [], opts, " (when a filename isn't specified)" );
run( 'validate', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], [], opts, ' (FRESH format)' ); run( 'validate', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], [], opts, ' (FRESH format)' );
run( 'validate', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' ); run( 'validate', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
function run( verb, src, dst, opts, msg ) { function run( verb, src, dst, opts, msg ) {
msg = msg || '.'; msg = msg || '.';

View File

@ -9,47 +9,37 @@ var chai = require('chai')
chai.config.includeStack = false; chai.config.includeStack = false;
describe('jane-doe.json (FRESH)', function () { function testResume(opts) {
describe( opts.title + ' (FRESH)', function () {
var _sheet; var _sheet;
it('should open without throwing an exception', function () { it('should open without throwing an exception', function () {
function tryOpen() { function tryOpen() {
_sheet = new FRESHResume().open( _sheet = new FRESHResume().open( opts.path );
'node_modules/jane-q-fullstacker/resume/jane-resume.json' );
} }
tryOpen.should.not.Throw(); tryOpen.should.not.Throw();
}); });
it('should have one or more of each section', function() { it('should have one or more of each section', function() {
expect( var newObj = _.pick( _sheet, opts.sections );
//(_sheet.basics) && expect( Object.keys(newObj).length ).to.equal( opts.sections.length );
_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() { it('should have a work duration of ' + opts.duration + ' years', function() {
_sheet.computed.numYears.should.equal( 7 ); _sheet.computed.numYears.should.equal( opts.duration );
}); });
it('should save without throwing an exception', function(){ it('should save without throwing an exception', function(){
function trySave() { function trySave() {
_sheet.save( 'tests/sandbox/jane-q-fullstacker.json' ); _sheet.save( 'test/sandbox/' + opts.title + '.json' );
} }
trySave.should.not.Throw(); trySave.should.not.Throw();
}); });
it('should not be modified after saving', function() { it('should not be modified after saving', function() {
var savedSheet = new FRESHResume().open('tests/sandbox/jane-q-fullstacker.json'); var savedSheet = new FRESHResume().open('test/sandbox/' + opts.title + '.json');
_sheet.stringify().should.equal( savedSheet.stringify() ); _sheet.stringify().should.equal( savedSheet.stringify() );
}); });
@ -64,4 +54,9 @@ describe('jane-doe.json (FRESH)', function () {
}); });
}); });
}
var sects = [ 'info', 'employment', 'service', 'skills', 'education', 'writing', 'recognition', 'references' ];
testResume({ title: 'jane-q-fullstacker', path: 'node_modules/jane-q-fullstacker/resume/jane-resume.json', duration: 7, sections: sects });
testResume({ title: 'johnny-trouble-resume', path: 'node_modules/johnny-trouble-resume/src/johnny-trouble.fresh.json', duration: 3, sections: sects });

View File

@ -38,13 +38,13 @@ function testResume( opts ) {
it('should save without throwing an exception', function() { it('should save without throwing an exception', function() {
var that = this; var that = this;
function trySave() { function trySave() {
_sheet.save( 'tests/sandbox/' + opts.title + '.json' ); _sheet.save( 'test/sandbox/' + opts.title + '.json' );
} }
trySave.should.not.Throw(); trySave.should.not.Throw();
}); });
it('should not be modified after saving', function() { it('should not be modified after saving', function() {
var savedSheet = new JRSResume().open( 'tests/sandbox/' + opts.title + '.json' ); var savedSheet = new JRSResume().open( 'test/sandbox/' + opts.title + '.json' );
_sheet.stringify().should.equal( savedSheet.stringify() ); _sheet.stringify().should.equal( savedSheet.stringify() );
}); });

View File

@ -1,5 +1,6 @@
var chai = require('chai') var SPAWNWATCHER = require('../src/core/spawn-watch')
, chai = require('chai')
, expect = chai.expect , expect = chai.expect
, should = chai.should() , should = chai.should()
, path = require('path') , path = require('path')
@ -11,7 +12,9 @@ var chai = require('chai')
chai.config.includeStack = false; chai.config.includeStack = false;
describe('Testing themes', function () { function genThemes( title, src, fmt ) {
describe('Testing themes against ' + title.toUpperCase() + ' resume ' + '(' + fmt + ')' , function () {
var _sheet; var _sheet;
@ -26,28 +29,40 @@ describe('Testing themes', function () {
useful: 'green', useful: 'green',
}); });
function genTheme( themeName ) { function genTheme( fmt, src, themeName, themeLoc, testTitle ) {
it( themeName.toUpperCase() + ' theme should generate without throwing an exception', function () { themeLoc = themeLoc || themeName;
testTitle = themeName.toUpperCase() + ' theme (' + fmt + ') should generate without throwing an exception';
it( testTitle, function () {
function tryOpen() { function tryOpen() {
var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; //var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json'];
var dst = ['tests/sandbox/' + themeName + '/resume.all']; var dst = ['test/sandbox/' + fmt + '/' + title + '/' + themeName + '/resume.all'];
var opts = { var opts = {
theme: themeName, theme: themeLoc,
format: 'FRESH', format: fmt,
prettify: true, prettify: true,
silent: true silent: true
}; };
FCMD.verbs.build( src, dst, opts, function() { } ); FCMD.verbs.build( src, dst, opts, function() {} );
} }
tryOpen.should.not.Throw(); tryOpen.should.not.Throw();
}); });
} }
genTheme('hello-world'); genTheme(fmt, src, 'hello-world');
genTheme('compact'); genTheme(fmt, src, 'compact');
genTheme('modern'); genTheme(fmt, src, 'modern');
genTheme('minimist'); genTheme(fmt, src, 'minimist');
genTheme('awesome'); genTheme(fmt, src, 'awesome');
genTheme('positive'); genTheme(fmt, src, 'positive');
genTheme(fmt, src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' );
genTheme(fmt, src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' );
genTheme(fmt, src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' );
genTheme(fmt, src, 'jsonresume-theme-classy', 'node_modules/jsonresume-theme-classy' );
}); });
}
genThemes( 'jane-q-fullstacker', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], 'FRESH' );
genThemes( 'johnny-trouble', ['node_modules/johnny-trouble-resume/src/johnny-trouble.fresh.json'], 'FRESH' );
genThemes( 'richard-hendriks', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], 'JRS' );