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

Compare commits

..

34 Commits

Author SHA1 Message Date
f4e763bd9c Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-08 12:28:45 -05:00
fbfff2a4e4 load theme partials for non html and doc
load global partials for html and doc only but load theme partials for
all outputs
2016-01-08 12:28:23 -05:00
1c93932737 Fix jsHint error. 2016-01-08 12:24:23 -05:00
cba29511bc Analyze: fix coverage percentage glitch. 2016-01-08 12:20:51 -05:00
1d655a4ddb Support duration units for JRS resumes. 2016-01-08 12:13:54 -05:00
ca94513630 Fix single format output error.
Fixes #97.
2016-01-08 11:59:10 -05:00
971d4a5439 Update FAQ and README. 2016-01-08 11:48:10 -05:00
f3dcbd9081 Improve error vs. warning formatting.
Errors = red. Warnings = yellow.
2016-01-08 10:42:24 -05:00
29c53af843 Rename "invalidTarget" error to "invalidFormat". 2016-01-08 10:09:46 -05:00
8d24087faa Rename src/gen --> src/generators. 2016-01-08 10:02:47 -05:00
95df8e5af4 Rename src/eng --> src/renderers
A renderer is a thing that renders or "paints" an arbitrary format using
a templating engine like Handlebars or Underscore. A generator is a
thing responsible for generating a given output format like HTML or MS
Word.
2016-01-08 09:59:47 -05:00
8a1da777b0 Bump version to 1.5.0. 2016-01-08 09:38:53 -05:00
44555da00f Fix PNG output format for JSON Resume themes. 2016-01-08 09:36:32 -05:00
46bd5d51cc Support implicit PDF generation (interim). 2016-01-08 09:00:43 -05:00
3964d300aa Update README. 2016-01-08 08:59:43 -05:00
d6280e6d89 Start integrating JRS and FRESH rendering paths. 2016-01-08 08:40:19 -05:00
4a2a47f551 Tweak casing. 2016-01-08 07:08:12 -05:00
ae51930c9c Tweak indentation. 2016-01-08 07:06:26 -05:00
fb33455bea Refactor JRS rendering. 2016-01-08 06:48:04 -05:00
28c703daf7 Improve error handling: PDFs. 2016-01-08 05:11:38 -05:00
0246a5da19 Remove html-pdf-generator class.
PDF generation now performed via html-pdf-cli-generator.
2016-01-07 18:34:43 -05:00
840d17c67b Wrap rasterize.js in IIFE / satisfy jsHint. 2016-01-07 18:33:26 -05:00
9f22e94cf7 Merge pull request #95 from aruberto/partials-fix
load theme partials for non html and doc
2016-01-07 18:30:54 -05:00
97ebecd84a Support CLI-based PDF generation.
Support Phantom and wkhtmltopdf generation via CLI.
2016-01-07 18:24:25 -05:00
96b9bb68e3 Introduce Phantom.js rasterizer script.
Via
https://raw.githubusercontent.com/ariya/phantomjs/master/examples/rasterize.js.
2016-01-07 17:53:42 -05:00
c5a5d3761d Remove explicit Phantom and wkhtmltopdf dependency.
Phantom is too heavy to impose on casual users and wkhtmltopdf errors
out on half the systems out there. We're better off speaking to both
tools, when present, via CLI or a secondary script.
2016-01-07 16:47:59 -05:00
c147403b1c load theme partials for non html and doc
load global partials for html and doc only but load theme partials for
all outputs
2016-01-07 16:39:46 -05:00
a2723452c2 Improve ENOENT handling. 2016-01-07 16:13:09 -05:00
cb3488276d Refactor error handling.
Work towards better debug/log/stack trace options for error cases.
2016-01-07 15:54:10 -05:00
43419c27cf Refactor API surface. 2016-01-07 13:44:39 -05:00
0f0c399dd5 Update CLI tests. 2016-01-07 13:12:21 -05:00
cb46497346 Rename generate.js to build.js.
Should match the canonical verb name -- "build". Generate is an alias.
2016-01-07 12:03:44 -05:00
850c640368 Annotate Phantom gen method. 2016-01-07 10:54:46 -05:00
60e455b36d Emit call stack for wkhtmltopdf errors. 2016-01-07 10:54:27 -05:00
33 changed files with 764 additions and 376 deletions

49
FAQ.md
View File

@ -40,7 +40,7 @@ FRESH themes currently come preinstalled with HackMyResume.
1. Specify the theme name in the `--theme` or `-t` parameter to the **build** command: 1. Specify the theme name in the `--theme` or `-t` parameter to the **build** command:
```bash ```bash
hackmyresume BUILD my-resume.json --theme <theme-name>` hackmyresume BUILD my-resume.json --theme <theme-name>
``` ```
`<theme-name>` can be one of `positive`, `compact`, `modern`, `minimist`, `hello-world`, or `awesome`. `<theme-name>` can be one of `positive`, `compact`, `modern`, `minimist`, `hello-world`, or `awesome`.
@ -90,8 +90,53 @@ hackmyresume BUILD resume.json -o myoptions.json
This ability is currently only supported for FRESH resume themes. This ability is currently only supported for FRESH resume themes.
## The HackMyResume terminal color scheme is giving me a headache! Can I disable it? ## The HackMyResume terminal color scheme is giving me a headache. Can I disable it?
Yes. Use the `--no-color` option to disable terminal colors: Yes. Use the `--no-color` option to disable terminal colors:
`hackmyresume <somecommand> <someoptions> --no-color` `hackmyresume <somecommand> <someoptions> --no-color`
## What's the difference between a FRESH theme and a JSON Resume theme?
FRESH themes are multiformat (HTML, Word, PDF, etc.) and required to support
Markdown formatting, configurable section titles, and various other features.
JSON Resume themes are typically HTML-driven, but capable of expansion to other
formats through tools. JSON Resume themes don't support Markdown natively, but
HMR does its best to apply your Markdown, when present, to any JSON Resume
themes it encounters.
## Do I have to have a FRESH resume to use a FRESH theme or a JSON Resume to use a JSON Resume theme?
No. You can mix and match FRESH and JRS-format themes freely. HackMyResume will
perform the necessary conversions on the fly.
## Can I build my own custom FRESH theme?
Yes. The easiest way is to copy an existing FRESH theme, like `modern` or
`compact`, and make your changes there. You can test your theme with:
```bash
hackmyresume build resume.json --theme path/to/my/theme/folder
```
## Can I build my own custom JSON Resume theme?
Yes. The easiest way is to copy an existing JSON Rsume theme and make your
changes there. You can test your theme with:
```bash
hackmyresume build resume.json --theme path/to/my/theme/folder
```
## Can I build my own tools / services / apps / websites around FRESH / FRESCA?
Yes! FRESH/FRESCA formats are 100% open source, permissively licensed under MIT,
and 100% free from company-specific, tool-specific, or commercially oriented
lock-in or cruft. These are clean formats designed for users and builders.
## Can I build my own tools / services / apps / websites around JSON Resume?
Yes! HackMyResume is not affiliated with JSON Resume, but like FRESH/FRESCA,
JSON Resume is open-source, permissively licensed, and free of proprietary
lock-in. See the JSON Resume website for details.

View File

@ -18,8 +18,10 @@ Use it to:
1. **Generate** HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, 1. **Generate** HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML,
YAML, print, smoke signal, carrier pigeon, and other arbitrary-format resumes YAML, print, smoke signal, carrier pigeon, and other arbitrary-format resumes
and CVs, from a single source of truth&mdash;without violating DRY. and CVs, from a single source of truth&mdash;without violating DRY.
2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats. 2. **Analyze** your resume for keyword density, gaps/overlaps, and other
3. **Validate** resumes against either format. metrics.
3. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats.
4. **Validate** resumes against either format.
HackMyResume is built with Node.js and runs on recent versions of OS X, Linux, HackMyResume is built with Node.js and runs on recent versions of OS X, Linux,
or Windows. View the [FAQ](FAQ.md). or Windows. View the [FAQ](FAQ.md).
@ -48,13 +50,22 @@ Install the latest stable version of HackMyResume with NPM:
[sudo] npm install hackmyresume -g [sudo] npm install hackmyresume -g
``` ```
Power users can install the latest bleeding-edge version (updated daily): Alternately, install the latest bleeding-edge version (updated daily):
```bash ```bash
[sudo] npm install hacksalot/hackmyresume#dev -g [sudo] npm install hacksalot/hackmyresume#dev -g
``` ```
**For PDF generation**, you'll need to install a copy of [wkhtmltopdf][3] and/or PhantomJS for your platform. ## Installing PDF Support (optional)
HackMyResume tries not to impose a specific PDF engine requirement on
the user, but will instead work with whatever PDF engines you have installed.
Currently, HackMyResume's PDF generation requires either [Phantom.js][2] or
[wkhtmltopdf][3] to be installed on your system and the `phantomjs` and/or
`wkhtmltopdf` binaries to be accessible on your PATH. This is an optional
requirement for users who care about PDF formats. If you don't care about PDF
formats, skip this step.
## Installing Themes ## Installing Themes

View File

@ -1,6 +1,6 @@
{ {
"name": "hackmyresume", "name": "hackmyresume",
"version": "1.4.2", "version": "1.5.0",
"description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.", "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",
@ -65,7 +65,6 @@
"moment": "^2.10.6", "moment": "^2.10.6",
"parse-filepath": "^0.6.3", "parse-filepath": "^0.6.3",
"path-exists": "^2.1.0", "path-exists": "^2.1.0",
"phantom": "^0.8.4",
"recursive-readdir-sync": "^1.0.6", "recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.2.0", "simple-html-tokenizer": "^0.2.0",
"slash": "^1.0.0", "slash": "^1.0.0",
@ -73,7 +72,6 @@
"string.prototype.startswith": "^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",
"word-wrap": "^1.1.0", "word-wrap": "^1.1.0",
"xml-escape": "^1.0.0", "xml-escape": "^1.0.0",
"yamljs": "^0.2.4" "yamljs": "^0.2.4"

View File

@ -6,15 +6,15 @@
module.exports = [ module.exports = [
{ name: 'html', ext: 'html', gen: new (require('../gen/html-generator'))() }, { name: 'html', ext: 'html', gen: new (require('../generators/html-generator'))() },
{ name: 'txt', ext: 'txt', gen: new (require('../gen/text-generator'))() }, { name: 'txt', ext: 'txt', gen: new (require('../generators/text-generator'))() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../gen/word-generator'))() }, { name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../generators/word-generator'))() },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new (require('../gen/html-pdf-generator'))() }, { name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new (require('../generators/html-pdf-cli-generator'))() },
{ name: 'png', ext: 'png', fmt: 'html', is: false, gen: new (require('../gen/html-png-generator'))() }, { name: 'png', ext: 'png', fmt: 'html', is: false, gen: new (require('../generators/html-png-generator'))() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new (require('../gen/markdown-generator'))() }, { name: 'md', ext: 'md', fmt: 'txt', gen: new (require('../generators/markdown-generator'))() },
{ name: 'json', ext: 'json', gen: new (require('../gen/json-generator'))() }, { name: 'json', ext: 'json', gen: new (require('../generators/json-generator'))() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../gen/json-yaml-generator'))() }, { name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../generators/json-yaml-generator'))() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../gen/latex-generator'))() } { name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../generators/latex-generator'))() }
]; ];

View File

@ -15,6 +15,7 @@ Error-handling routines for HackMyResume.
, FS = require('fs') , FS = require('fs')
, FCMD = require('../hackmyapi') , FCMD = require('../hackmyapi')
, PATH = require('path') , PATH = require('path')
, WRAP = require('word-wrap')
, chalk = require('chalk'); , chalk = require('chalk');
@ -28,101 +29,142 @@ Error-handling routines for HackMyResume.
err: function( ex, shouldExit ) { err: function( ex, shouldExit ) {
var msg = '', exitCode;
var msg = '', exitCode, log = console.log, showStack = ex.showStack;
// If the exception has been handled elsewhere and shouldExit is true,
// let's get out of here, otherwise silently return.
if( ex.handled ) { if( ex.handled ) {
if( shouldExit ) if( shouldExit )
process.exit( exitCode ); process.exit( exitCode );
return; return;
} }
// Get an error message -- either a HackMyResume error message or the
// exception's associated error message
if( ex.fluenterror ){ if( ex.fluenterror ){
var errInfo = get_error_msg( ex );
switch( ex.fluenterror ) { msg = errInfo.msg;
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 = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
chalk.yellow(' in FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.missingCommand:
msg = chalk.yellow("Please ") + chalk.yellow.bold("give me a command") +
chalk.yellow(" (");
msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? chalk.yellow('or ') : '') +
chalk.yellow.bold(v.toUpperCase());
}).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += chalk.gray(FS.readFileSync( PATH.resolve(__dirname, '../use.txt'), 'utf8' ));
break;
case HACKMYSTATUS.invalidCommand:
msg = chalk.yellow('Invalid command: "') + chalk.yellow.bold(ex.attempted) + chalk.yellow('"');
break;
case HACKMYSTATUS.resumeNotFoundAlt:
msg = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
chalk.yellow(' in either FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.inputOutputParity:
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify an output file name') +
chalk.yellow(' for every input file you wish to convert.');
break;
case HACKMYSTATUS.createNameMissing:
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify the filename of the resume') +
chalk.yellow(' to create.');
break;
case HACKMYSTATUS.wkhtmltopdf:
msg = chalk.red.bold('ERROR: PDF generation failed. ') + chalk.red('Make sure wkhtmltopdf is ' +
'installed and accessible from your path.');
break;
case HACKMYSTATUS.invalid:
msg = chalk.red.bold('ERROR: Validation failed and the --assert option was specified.');
break;
}
exitCode = ex.fluenterror; exitCode = ex.fluenterror;
showStack = errInfo.showStack;
} }
else { else {
msg = ex.toString(); msg = ex.toString();
exitCode = 4; exitCode = -1;
// Deal with pesky 'Error:' prefix.
var idx = msg.indexOf('Error: ');
msg = idx === -1 ? msg : msg.substring( idx + 7 );
} }
// Deal with pesky 'Error:' prefix. // Log non-HackMyResume-handled errors in red with ERROR prefix. Log HMR
var idx = msg.indexOf('Error: '); // errors as-is.
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); ex.fluenterror ?
log( msg.toString() ) :
log( chalk.red.bold('ERROR: ' + msg.toString()) );
// If this is an unhandled error, or a specific class of handled error, // Usually emit the stack
// output the error message and stack. ( showStack && ex.code !== 'ENOENT' ) && log( chalk.gray(ex.stack) );
if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s
console.log( chalk.red.bold('ERROR: ' + trimmed.toString()) );
if( ex.code !== 'ENOENT' ) // Don't emit stack for common stuff
console.log( chalk.gray(ex.stack) );
}
else {
console.log( trimmed.toString() );
}
// Let the error code be the process's return code. // Let the error code be the process's return code.
if( shouldExit || ex.shouldExit ) ( shouldExit || ex.shouldExit ) && process.exit( exitCode );
process.exit( exitCode );
} }
}; };
function get_error_msg( ex ) {
var msg = '', withStack = false, isError = false;
switch( ex.fluenterror ) {
case HACKMYSTATUS.themeNotFound:
msg = formatWarning(
chalk.bold("Couldn't find the '" + ex.data + "' theme."),
" Please specify the name of a preinstalled FRESH theme " +
"or the path to a locally installed FRESH or JSON Resume theme.");
break;
case HACKMYSTATUS.copyCSS:
msg = formatWarning("Couldn't copy CSS file to destination folder.");
break;
case HACKMYSTATUS.resumeNotFound:
msg = formatWarning('Please ' + chalk.bold('feed me a resume') +
' in FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.missingCommand:
msg = formatWarning("Please " +chalk.bold("give me a command") + " (");
msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? chalk.yellow('or ') : '') +
chalk.yellow.bold(v.toUpperCase());
}).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += chalk.gray(FS.readFileSync(
PATH.resolve(__dirname, '../use.txt'), 'utf8' ));
break;
case HACKMYSTATUS.invalidCommand:
msg = formatWarning('Invalid command: "'+chalk.bold(ex.attempted)+'"');
break;
case HACKMYSTATUS.resumeNotFoundAlt:
msg = formatWarning('Please ' + chalk.bold('feed me a resume') +
' in either FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.inputOutputParity:
msg = formatWarning('Please ' +
chalk.bold('specify an output file name') +
' for every input file you wish to convert.');
break;
case HACKMYSTATUS.createNameMissing:
msg = formatWarning('Please ' +
chalk.bold('specify the filename of the resume') + ' to create.');
break;
case HACKMYSTATUS.pdfGeneration:
msg = formatError(chalk.bold('ERROR: PDF generation failed. ') +
'Make sure wkhtmltopdf is installed and accessible from your path.');
if( ex.inner ) msg += chalk.red('\n' + ex.inner);
withStack = true;
break;
case HACKMYSTATUS.invalid:
msg = formatError('Validation failed and the --assert option was ' +
'specified.');
break;
case HACKMYSTATUS.invalidFormat:
ex.data.forEach(function(d){ msg +=
formatWarning('The ' + chalk.bold(ex.theme.name.toUpperCase()) +
" theme doesn't support the " + chalk.bold(d.format.toUpperCase()) +
" format.\n");
});
break;
case HACKMYSTATUS.notOnPath:
msg = formatError( ex.engine + " wasn't found on your system path or" +
" is inaccessible. PDF not generated." );
break;
}
return {
msg: msg,
withStack: withStack
};
}
function formatError( msg ) {
return chalk.red.bold( 'ERROR: ' + msg );
}
function formatWarning( brief, msg ) {
return chalk.yellow(brief) + chalk.yellow(msg || '');
}
}()); }());

View File

@ -274,7 +274,8 @@ Definition of the JRSResume class.
*latest end date of all jobs in the work history*. This last condition is for *latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs. sheets that have overlapping jobs.
*/ */
JRSResume.prototype.duration = function() { JRSResume.prototype.duration = function( unit ) {
unit = unit || 'years';
if( this.work && this.work.length ) { if( this.work && this.work.length ) {
var careerStart = this.work[ this.work.length - 1].safeStartDate; var careerStart = this.work[ this.work.length - 1].safeStartDate;
if ((typeof careerStart === 'string' || careerStart instanceof String) && if ((typeof careerStart === 'string' || careerStart instanceof String) &&
@ -283,7 +284,7 @@ Definition of the JRSResume class.
var careerLast = _.max( this.work, function( w ) { var careerLast = _.max( this.work, function( w ) {
return w.safeEndDate.unix(); return w.safeEndDate.unix();
}).safeEndDate; }).safeEndDate;
return careerLast.diff( careerStart, 'years' ); return careerLast.diff( careerStart, unit );
} }
return 0; return 0;
}; };

View File

@ -16,8 +16,7 @@ Definition of the JRSTheme class.
/** /**
The JRSTheme class is a representation of a JSON Resume The JRSTheme class is a representation of a JSON Resume theme asset.
theme asset. See also: FRESHTheme.
@class JRSTheme @class JRSTheme
*/ */
function JRSTheme() { function JRSTheme() {
@ -41,16 +40,40 @@ Definition of the JRSTheme class.
// Open and parse the theme's package.json file. // Open and parse the theme's package.json file.
var pkgJsonPath = PATH.join( thFolder, 'package.json' ); var pkgJsonPath = PATH.join( thFolder, 'package.json' );
if( pathExists( pkgJsonPath )) { if( pathExists( pkgJsonPath )) {
var thApi = require( thFolder ) var thApi = require( thFolder )
, thPkg = require( pkgJsonPath ); , thPkg = require( pkgJsonPath );
this.name = thPkg.name; this.name = thPkg.name;
this.render = (thApi && thApi.render) || undefined; this.render = (thApi && thApi.render) || undefined;
this.engine = 'jrs';
// Create theme formats (HTML and PDF). Just add the bare minimum mix of
// properties necessary to allow JSON Resume themes to share a rendering
// path with FRESH themes.
this.formats = { this.formats = {
html: { title:'html', outFormat:'html', ext:'html' } html: { outFormat: 'html', files: [
{
action: 'transform',
render: this.render,
major: true,
ext: 'html',
css: null
}
]},
pdf: { outFormat: 'pdf', files: [
{
action: 'transform',
render: this.render,
major: true,
ext: 'pdf',
css: null
}
]}
}; };
} }
else { else {
throw { fluenterror: 10 }; throw { fluenterror: HACKMYSTATUS.missingPackageJSON };
} }
return this; return this;

View File

@ -116,8 +116,10 @@ Definition of the ResumeFactory class.
ex.handled = true; ex.handled = true;
} }
if( opts.throw ) throw ex; // FS.readFileSync failed
else return { if( !rawData || opts.throw ) throw ex;
return {
error: ex, error: ex,
raw: rawData, raw: rawData,
file: fileName file: fileName

View File

@ -16,9 +16,11 @@ Status codes for HackMyResume.
resumeNotFoundAlt: 6, resumeNotFoundAlt: 6,
inputOutputParity: 7, inputOutputParity: 7,
createNameMissing: 8, createNameMissing: 8,
wkhtmltopdf: 9, pdfgeneration: 9,
missingPackageJSON: 10, missingPackageJSON: 10,
invalid: 11 invalid: 11,
invalidFormat: 12,
notOnPath: 13
}; };
}()); }());

View File

@ -1,81 +0,0 @@
/**
Definition of the HtmlPdfGenerator class.
@module html-pdf-generator.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var TemplateGenerator = require('./template-generator')
, FS = require('fs-extra')
, HTML = require( 'html' );
/**
An HTML-driven PDF resume generator for HackMyResume.
*/
var HtmlPdfGenerator = module.exports = TemplateGenerator.extend({
init: function() {
this._super( 'pdf', 'html' );
},
/**
Generate the binary PDF.
*/
onBeforeSave: function( info ) {
engines[ info.opts.pdf || 'wkhtmltopdf' ]
.call( this, info.mk, info.outputFile );
return null; // halt further processing
}
});
var engines = {
/**
Generate a PDF from HTML using wkhtmltopdf.
*/
wkhtmltopdf: function(markup, fOut) {
var wk;
try {
wk = require('wkhtmltopdf');
wk( markup, { pageSize: 'letter' } )
.pipe( FS.createWriteStream( fOut ) );
}
catch(ex) {
// { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', ... }
// { [Error: ENOENT] }
throw { fluenterror: this.codes.wkhtmltopdf };
}
},
/**
Generate a PDF from HTML using Phantom.
*/
phantom: function( markup, fOut ) {
require('phantom').create( function( ph ) {
ph.createPage( function( page ) {
page.setContent( markup );
page.set('paperSize', {
format: 'A4',
orientation: 'portrait',
margin: '1cm'
});
page.set("viewportSize", {
width: 1024, // TODO: option-ify
height: 768 // TODO: Use "A" sizes
});
page.set('onLoadFinished', function(success) {
page.render( fOut );
ph.exit();
});
},
{ dnodeOpts: { weak: false } } );
});
}
};
}());

View File

@ -0,0 +1,139 @@
/**
Definition of the HtmlPdfCLIGenerator class.
@module html-pdf-generator.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var TemplateGenerator = require('./template-generator')
, FS = require('fs-extra')
, HTML = require( 'html' )
, PATH = require('path')
, SLASH = require('slash');
/**
An HTML-driven PDF resume generator for HackMyResume. Talks to Phantom,
wkhtmltopdf, and other PDF engines over a CLI (command-line interface).
If an engine isn't installed for a particular platform, error out gracefully.
*/
var HtmlPdfCLIGenerator = module.exports = TemplateGenerator.extend({
init: function() {
this._super( 'pdf', 'html' );
},
/**
Generate the binary PDF.
*/
onBeforeSave: function( info ) {
try {
var safe_eng = info.opts.pdf || 'wkhtmltopdf';
engines[ safe_eng ].call( this, info.mk, info.outputFile );
return null; // halt further processing
}
catch(ex) {
// { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', ... }
// { [Error: ENOENT] }
throw ( ex.inner && ex.inner.code === 'ENOENT' ) ?
{ fluenterror: this.codes.notOnPath, engine: ex.cmd } :
{ fluenterror: this.codes.pdfGeneration, inner: ex.inner };
}
}
});
// TODO: Move each engine to a separate module
var engines = {
/**
Generate a PDF from HTML using wkhtmltopdf's CLI interface.
Spawns a child process with `wkhtmltopdf <source> <target>`. wkhtmltopdf
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease wkhtmltopdf rendering
*/
wkhtmltopdf: function(markup, fOut) {
// Save the markup to a temporary file
var tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync( tempFile, markup, 'utf8' );
var spawn = require('child_process').spawnSync;
var info = spawn('wkhtmltopdf', [
tempFile, fOut
]);
if( info.error ) {
throw {
cmd: 'wkhtmltopdf',
inner: info.error
};
}
// child.stdout.on('data', function(chunk) {
// // output will be here in chunks
// });
// or if you want to send output elsewhere
//child.stdout.pipe(dest);
},
/**
Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
*/
phantom: function( markup, fOut ) {
// Save the markup to a temporary file
var tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync( tempFile, markup, 'utf8' );
var scriptPath = SLASH( PATH.relative( process.cwd(),
PATH.resolve( __dirname, '../utils/rasterize.js' ) ) );
var sourcePath = SLASH( PATH.relative( process.cwd(), tempFile) );
var destPath = SLASH( PATH.relative( process.cwd(), fOut) );
var spawn = require('child_process').spawnSync;
var info = spawn('phantomjs', [ scriptPath, sourcePath, destPath ]);
if( info.error ) {
throw {
cmd: 'phantomjs',
inner: info.error
};
}
// child.stdout.on('data', function(chunk) {
// // output will be here in chunks
// });
//
// // or if you want to send output elsewhere
// child.stdout.pipe(dest);
}
};
}());

View File

@ -4,25 +4,37 @@ Definition of the HtmlPngGenerator class.
@module html-png-generator.js @module html-png-generator.js
*/ */
(function() { (function() {
var TemplateGenerator = require('./template-generator') var TemplateGenerator = require('./template-generator')
, FS = require('fs-extra') , FS = require('fs-extra')
, HTML = require( 'html' ); , HTML = require( 'html' );
/** /**
An HTML-based PNG resume generator for HackMyResume. An HTML-based PNG resume generator for HackMyResume.
*/ */
var HtmlPngGenerator = module.exports = TemplateGenerator.extend({ var HtmlPngGenerator = module.exports = TemplateGenerator.extend({
init: function() { init: function() {
this._super( 'png', 'html' ); this._super( 'png', 'html' );
}, },
invoke: function( rez, themeMarkup, cssInfo, opts ) { invoke: function( rez, themeMarkup, cssInfo, opts ) {
//return YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 ); // TODO: Not currently called or callable.
}, },
generate: function( rez, f, opts ) { generate: function( rez, f, opts ) {
var htmlResults = opts.targets.filter(function(t){ var htmlResults = opts.targets.filter(function(t){
return t.fmt.outFormat === 'html'; return t.fmt.outFormat === 'html';
@ -30,18 +42,25 @@ Definition of the HtmlPngGenerator class.
var htmlFile = htmlResults[0].final.files.filter(function(fl){ var htmlFile = htmlResults[0].final.files.filter(function(fl){
return fl.info.ext === 'html'; return fl.info.ext === 'html';
}); });
png(htmlFile[0].data, f); png( htmlFile[0].data, f );
} }
}); });
/** /**
Generate a PNG from HTML. Generate a PNG from HTML.
*/ */
function png( markup, fOut ) { function png( markup, fOut ) {
// TODO: Which Webshot syntax?
// 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) { } ); require('webshot')( markup , fOut, { siteType: 'html' }, function(err) { } );
} }
}()); }());

View File

@ -1,5 +1,5 @@
/** /**
Definition of the TemplateGenerator class. Definition of the TemplateGenerator class. TODO: Refactor
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@module template-generator.js @module template-generator.js
*/ */
@ -145,6 +145,10 @@ Definition of the TemplateGenerator class.
var thisFilePath; var thisFilePath;
if( theme.engine === 'jrs' ) {
file.info.orgPath = '';
}
if( file.info.action === 'transform' ) { if( file.info.action === 'transform' ) {
thisFilePath = PATH.join( outFolder, file.info.orgPath ); thisFilePath = PATH.join( outFolder, file.info.orgPath );
try { try {
@ -175,7 +179,8 @@ Definition of the TemplateGenerator class.
FS.copySync( file.info.path, thisFilePath ); FS.copySync( file.info.path, thisFilePath );
} }
catch(ex) { catch(ex) {
console.log(ex); ex.showStack = true;
require('../core/error-handler').err( ex );
} }
} }
}); });
@ -208,7 +213,7 @@ Definition of the TemplateGenerator class.
single: function( json, jst, format, cssInfo, opts, theme ) { single: function( json, jst, format, cssInfo, opts, theme ) {
this.opts.freezeBreaks && ( jst = freeze(jst) ); this.opts.freezeBreaks && ( jst = freeze(jst) );
var eng = require( '../eng/' + theme.engine + '-generator' ); var eng = require( '../renderers/' + theme.engine + '-generator' );
var result = eng.generate( json, jst, format, cssInfo, opts, theme ); var result = eng.generate( json, jst, format, cssInfo, opts, theme );
this.opts.freezeBreaks && ( result = unfreeze(result) ); this.opts.freezeBreaks && ( result = unfreeze(result) );
@ -274,8 +279,8 @@ Definition of the TemplateGenerator class.
theme ); theme );
} }
catch(ex) { catch(ex) {
console.log(ex); ex.showStack = true;
throw ex; require('../core/error-handler').err( ex );
} }
} }

View File

@ -1,24 +1,29 @@
/** /**
External API surface for HackMyResume. External API surface for HackMyResume.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
@module hackmyapi.js @module hackmyapi.js
*/ */
(function() { (function() {
var v = {
build: require('./verbs/generate'),
analyze: require('./verbs/analyze'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
new: require('./verbs/create')
};
/**
The formal HackMyResume API.
*/
var HackMyAPI = module.exports = { var HackMyAPI = module.exports = {
verbs: v, verbs: {
build: require('./verbs/build'),
analyze: require('./verbs/analyze'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
new: require('./verbs/create')
},
alias: { alias: {
generate: v.build, generate: require('./verbs/build'),
create: v.new create: require('./verbs/create')
}, },
options: require('./core/default-options'), options: require('./core/default-options'),
formats: require('./core/default-formats'), formats: require('./core/default-formats'),
@ -28,16 +33,18 @@ External API surface for HackMyResume.
FRESHTheme: require('./core/fresh-theme'), FRESHTheme: require('./core/fresh-theme'),
JRSTheme: require('./core/jrs-theme'), JRSTheme: require('./core/jrs-theme'),
FluentDate: require('./core/fluent-date'), FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./gen/html-generator'), HtmlGenerator: require('./generators/html-generator'),
TextGenerator: require('./gen/text-generator'), TextGenerator: require('./generators/text-generator'),
HtmlPdfGenerator: require('./gen/html-pdf-generator'), HtmlPdfCliGenerator: require('./generators/html-pdf-cli-generator'),
WordGenerator: require('./gen/word-generator'), WordGenerator: require('./generators/word-generator'),
MarkdownGenerator: require('./gen/markdown-generator'), MarkdownGenerator: require('./generators/markdown-generator'),
JsonGenerator: require('./gen/json-generator'), JsonGenerator: require('./generators/json-generator'),
YamlGenerator: require('./gen/yaml-generator'), YamlGenerator: require('./generators/yaml-generator'),
JsonYamlGenerator: require('./gen/json-yaml-generator'), JsonYamlGenerator: require('./generators/json-yaml-generator'),
LaTeXGenerator: require('./gen/latex-generator'), LaTeXGenerator: require('./generators/latex-generator'),
HtmlPngGenerator: require('./gen/html-png-generator') HtmlPngGenerator: require('./generators/html-png-generator')
}; };
}()); }());

View File

@ -86,15 +86,19 @@ Employment gap analysis for HackMyResume.
// When the reference count is > 0, the candidate is employed. When the // When the reference count is > 0, the candidate is employed. When the
// reference count reaches 2, the candidate is overlapped. // reference count reaches 2, the candidate is overlapped.
var num_gaps = 0, ref_count = 0, total_gap_days = 0, total_work_days = 0 var num_gaps = 0, ref_count = 0, total_gap_days = 0, gap_start;
, gap_start;
new_e.forEach( function(point) { new_e.forEach( function(point) {
var inc = point[0] === 'start' ? 1 : -1; var inc = point[0] === 'start' ? 1 : -1;
ref_count += inc; ref_count += inc;
// If the ref count just reached 0, start a new GAP
if( ref_count === 0 ) { if( ref_count === 0 ) {
coverage.gaps.push( { start: point[1], end: null }); coverage.gaps.push( { start: point[1], end: null });
} }
// If the ref count reached 1 by rising, end the last GAP
else if( ref_count === 1 && inc === 1 ) { else if( ref_count === 1 && inc === 1 ) {
var lastGap = _.last( coverage.gaps ); var lastGap = _.last( coverage.gaps );
if( lastGap ) { if( lastGap ) {
@ -103,9 +107,13 @@ Employment gap analysis for HackMyResume.
total_gap_days += lastGap.duration; total_gap_days += lastGap.duration;
} }
} }
// If the ref count reaches 2 by rising, start a new OVERLAP
else if( ref_count === 2 && inc === 1 ) { else if( ref_count === 2 && inc === 1 ) {
coverage.overlaps.push( { start: point[1], end: null }); coverage.overlaps.push( { start: point[1], end: null });
} }
// If the ref count reaches 1 by falling, end the last OVERLAP
else if( ref_count === 1 && inc === -1 ) { else if( ref_count === 1 && inc === -1 ) {
var lastOver = _.last( coverage.overlaps ); var lastOver = _.last( coverage.overlaps );
if( lastOver ) { if( lastOver ) {
@ -114,32 +122,39 @@ Employment gap analysis for HackMyResume.
if( lastOver.duration === 0 ) { if( lastOver.duration === 0 ) {
coverage.overlaps.pop(); coverage.overlaps.pop();
} }
total_work_days += lastOver.duration;
} }
} }
}); });
// It's possible that the last overlap didn't have an explicit .end date. // It's possible that the last gap/overlap didn't have an explicit .end
// If so, set the end date to the present date and compute the overlap // date.If so, set the end date to the present date and compute the
// duration normally. // duration normally.
if( coverage.overlaps.length ) { if( coverage.overlaps.length ) {
if( !_.last( coverage.overlaps ).end ) { var o = _.last( coverage.overlaps );
var l = _.last( coverage.overlaps ); if( o && !o.end ) {
l.end = moment(); o.end = moment();
l.duration = l.end.diff( l.start, 'days' ); o.duration = o.end.diff( o.start, 'days' );
}
}
if( coverage.gaps.length ) {
var g = _.last( coverage.gaps );
if( g && !g.end ) {
g.end = moment();
g.duration = g.end.diff( g.start, 'days' );
} }
} }
// Package data for return to the client
var tdur = rez.duration('days');
var dur = { var dur = {
total: rez.duration('days'), total: tdur,
work: total_work_days, work: tdur - total_gap_days,
gaps: total_gap_days gaps: total_gap_days
}; };
coverage.pct = ( dur.total > 0 && dur.work > 0 ) ? coverage.pct = ( dur.total > 0 && dur.work > 0 ) ?
((((dur.total - dur.gaps) / dur.total) * 100)).toFixed(1) + '%' : ((((dur.total - dur.gaps) / dur.total) * 100)).toFixed(1) + '%' :
'???'; '???';
coverage.duration = dur; coverage.duration = dur;
return coverage; return coverage;
} }

View File

@ -61,30 +61,31 @@ Definition of the HandlebarsGenerator class.
function registerPartials(format, theme) { function registerPartials(format, theme) {
if( format !== 'html' && format != 'doc' ) if( format === 'html' || format === 'doc' ) {
return;
// Locate the global partials folder // Locate the global partials folder
var partialsFolder = PATH.join( var partialsFolder = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname, parsePath( require.resolve('fresh-themes') ).dirname,
'/partials/', '/partials/',
format format
); );
// Register global partials in the /partials folder // Register global partials in the /partials folder
// TODO: Only do this once per HMR invocation. // TODO: Only do this once per HMR invocation.
_.each( READFILES( partialsFolder, function(error){ }), function( el ) { _.each( READFILES( partialsFolder, function(error){ }), function( el ) {
var pathInfo = parsePath( el ); var pathInfo = parsePath( el );
var name = SLASH( PATH.relative( partialsFolder, el ) var name = SLASH( PATH.relative( partialsFolder, el )
.replace(/\.html$|\.xml$/, '') ); .replace(/\.html$|\.xml$/, '') );
if( pathInfo.dirname.endsWith('section') ) { if( pathInfo.dirname.endsWith('section') ) {
name = SLASH(name.replace(/\.html$|\.xml$/, '')); name = SLASH(name.replace(/\.html$|\.xml$/, ''));
} }
var tplData = FS.readFileSync( el, 'utf8' ); var tplData = FS.readFileSync( el, 'utf8' );
var compiledTemplate = HANDLEBARS.compile( tplData ); var compiledTemplate = HANDLEBARS.compile( tplData );
HANDLEBARS.registerPartial( name, compiledTemplate ); HANDLEBARS.registerPartial( name, compiledTemplate );
theme.partialsInitialized = true; theme.partialsInitialized = true;
}); });
}
// Register theme-specific partials // Register theme-specific partials
_.each( theme.partials, function( el ) { _.each( theme.partials, function( el ) {

View File

@ -0,0 +1,76 @@
/**
Definition of the JRSGenerator class.
@license MIT. See LICENSE.md for details.
@module jrs-generator.js
*/
(function() {
var _ = require('underscore')
, HANDLEBARS = require('handlebars')
, FS = require('fs')
, registerHelpers = require('./handlebars-helpers')
, PATH = require('path')
, parsePath = require('parse-filepath')
, READFILES = require('recursive-readdir-sync')
, SLASH = require('slash')
, MD = require('marked');
/**
Perform template-based resume generation for JSON Resume themes.
@class JRSGenerator
*/
var JRSGenerator = module.exports = {
generate: function( json, jst, format, cssInfo, opts, theme ) {
// JSON Resume themes don't have a specific structure, so the safest thing
// to do is copy all files from source to dest.
// 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);
// // }
// });
// Disable JRS theme chatter (console.log, console.error, etc.)
var off = ['log', 'error', 'dir'], org = off.map(function(c){
var ret = console[c]; console[c] = function(){}; return ret;
});
// Freeze and render
var rezHtml = theme.render( json.harden() );
// Turn logging back on
off.forEach(function(c, idx){ console[c] = org[idx]; });
// Unfreeze and apply Markdown
rezHtml = rezHtml.replace( /@@@@~.*?~@@@@/gm, function(val){
return MDIN( val.replace( /~@@@@/gm,'' ).replace( /@@@@~/gm,'' ) );
});
return rezHtml;
}
};
function MDIN(txt) { // TODO: Move this
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
}());

56
src/utils/rasterize.js Normal file
View File

@ -0,0 +1,56 @@
// Exemplar script for generating documents with Phantom.js.
// https://raw.githubusercontent.com/ariya/phantomjs/master/examples/rasterize.js
(function() {
"use strict";
var page = require('webpage').create(),
system = require('system'),
address, output, size;
if (system.args.length < 3 || system.args.length > 5) {
console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
console.log(' image (png/jpg output) examples: "1920px" entire page, window width 1920px');
console.log(' "800px*600px" window, clipped to 800x600');
phantom.exit(1);
} else {
address = system.args[1];
output = system.args[2];
page.viewportSize = { width: 600, height: 600 };
if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
size = system.args[3].split('*');
page.paperSize = size.length === 2 ? { width: size[0], height: size[1], margin: '0px' }
: { format: system.args[3], orientation: 'portrait', margin: '1cm' };
} else if (system.args.length > 3 && system.args[3].substr(-2) === "px") {
size = system.args[3].split('*');
if (size.length === 2) {
pageWidth = parseInt(size[0], 10);
pageHeight = parseInt(size[1], 10);
page.viewportSize = { width: pageWidth, height: pageHeight };
page.clipRect = { top: 0, left: 0, width: pageWidth, height: pageHeight };
} else {
console.log("size:", system.args[3]);
pageWidth = parseInt(system.args[3], 10);
pageHeight = parseInt(pageWidth * 3/4, 10); // it's as good an assumption as any
console.log ("pageHeight:",pageHeight);
page.viewportSize = { width: pageWidth, height: pageHeight };
}
}
if (system.args.length > 4) {
page.zoomFactor = system.args[4];
}
page.open(address, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit(1);
} else {
window.setTimeout(function () {
page.render(output);
phantom.exit();
}, 200);
}
});
}
}());

View File

@ -15,6 +15,7 @@ Implementation of the 'generate' verb for HackMyResume.
, MD = require('marked') , MD = require('marked')
, MKDIRP = require('mkdirp') , MKDIRP = require('mkdirp')
, EXTEND = require('../utils/extend') , EXTEND = require('../utils/extend')
, HACKMYSTATUS = require('../core/status-codes')
, parsePath = require('parse-filepath') , parsePath = require('parse-filepath')
, _opts = require('../core/default-options') , _opts = require('../core/default-options')
, FluentTheme = require('../core/fresh-theme') , FluentTheme = require('../core/fresh-theme')
@ -29,15 +30,6 @@ Implementation of the 'generate' verb for HackMyResume.
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
/** /**
Given a source resume in FRESH or JRS format, a destination resume path, and a Given a source resume in FRESH or JRS format, a destination resume path, and a
theme file, generate 0..N resumes in the desired formats. theme file, generate 0..N resumes in the desired formats.
@ -48,29 +40,18 @@ Implementation of the 'generate' verb for HackMyResume.
*/ */
function build( src, dst, opts, logger, errHandler ) { function build( src, dst, opts, logger, errHandler ) {
// Housekeeping prep( src, dst, opts, logger, errHandler );
//_opts = extend( true, _opts, opts );
_log = logger || console.log;
_err = errHandler || error;
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
_opts.css = opts.css || 'embed';
_opts.pdf = opts.pdf;
_opts.wrap = opts.wrap || 60;
_opts.stitles = opts.sectionTitles;
_opts.tips = opts.tips;
//_opts.noTips = opts.noTips;
// If two or more files are passed to the GENERATE command and the TO
// keyword is omitted, the last file specifies the output file.
if( src.length > 1 && ( !dst || !dst.length ) ) {
dst.push( src.pop() );
}
// Load the theme...we do this first because the theme choice (FRESH or // Load the theme...we do this first because the theme choice (FRESH or
// JSON Resume) determines what format we'll convert the resume to. // JSON Resume) determines what format we'll convert the resume to.
var tFolder = verify_theme( _opts.theme ); var tFolder = verifyTheme( _opts.theme );
var theme = load_theme( tFolder ); var theme = loadTheme( tFolder );
// Check for invalid outputs
var inv = verifyOutputs( dst, theme );
if( inv && inv.length ) {
throw {fluenterror: HACKMYSTATUS.invalidFormat, data: inv, theme: theme};
}
// Load input resumes... // Load input resumes...
if( !src || !src.length ) { throw { fluenterror: 3 }; } if( !src || !src.length ) { throw { fluenterror: 3 }; }
@ -134,46 +115,72 @@ Implementation of the 'generate' verb for HackMyResume.
/**
Prepare for a BUILD run.
*/
function prep( src, dst, opts, logger, errHandler ) {
// Housekeeping
_log = logger || console.log;
_err = errHandler || error;
// Cherry-pick options //_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
_opts.prettify = opts.prettify === true;
_opts.css = opts.css || 'embed';
_opts.pdf = opts.pdf;
_opts.wrap = opts.wrap || 60;
_opts.stitles = opts.sectionTitles;
_opts.tips = opts.tips;
_opts.noTips = opts.noTips;
// If two or more files are passed to the GENERATE command and the TO
// keyword is omitted, the last file specifies the output file.
( src.length > 1 && ( !dst || !dst.length ) ) && dst.push( src.pop() );
}
/** /**
Generate a single target resume such as "out/rez.html" or "out/rez.doc". Generate a single target resume such as "out/rez.html" or "out/rez.doc".
TODO: Refactor.
@param targInfo Information for the target resume. @param targInfo Information for the target resume.
@param theme A FRESHTheme or JRSTheme object. @param theme A FRESHTheme or JRSTheme object.
@returns
*/ */
function single( targInfo, theme, finished ) { function single( targInfo, theme, finished ) {
function MDIN(txt) { // TODO: Move this
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
try { try {
if( !targInfo.fmt ) {
return;
}
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;
var suffix = ''; var suffix = '';
if( targInfo.fmt.outFormat === 'pdf' ) { if( targInfo.fmt.outFormat === 'pdf' ) {
if( _opts.pdf ) { if( _opts.pdf ) {
if( _opts.pdf !== 'none' ) { if( _opts.pdf !== 'none' ) {
suffix = chalk.green(' (with ' + _opts.pdf + ')'); suffix = chalk.green(' (with ' + _opts.pdf + ')');
} }
else { else {
_log( chalk.gray('Skipping ') + _log( chalk.gray('Skipping ') +
chalk.white.bold( chalk.white.bold(
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) + pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
chalk.gray(' resume') + suffix + chalk.green(': ') + chalk.gray(' resume') + suffix + chalk.green(': ') +
chalk.white( PATH.relative(process.cwd(), f )) ); chalk.white( PATH.relative(process.cwd(), f )) );
return; return;
}
} }
} }
}
_log( chalk.green('Generating ') + _log( chalk.green('Generating ') +
chalk.green.bold( chalk.green.bold(
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) + pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
chalk.green(' resume') + suffix + chalk.green(': ') + chalk.green(' resume') + suffix + chalk.green(': ') +
chalk.green.bold( PATH.relative(process.cwd(), f )) ); chalk.green.bold( PATH.relative(process.cwd(), f )) );
// If targInfo.fmt.files exists, this format is backed by a document. // If targInfo.fmt.files exists, this format is backed by a document.
// Fluent/FRESH themes are handled here. // Fluent/FRESH themes are handled here.
@ -185,8 +192,8 @@ Implementation of the 'generate' verb for HackMyResume.
return theFormat.gen.generate( rez, f, _opts ); return theFormat.gen.generate( rez, f, _opts );
} }
// Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format //Otherwise this is an ad-hoc format (JSON, YML, or PNG) that every theme
// (JSON, YML, or PNG) that every theme gets "for free". // gets "for free".
else { else {
theFormat = _fmts.filter( function(fmt) { theFormat = _fmts.filter( function(fmt) {
@ -196,43 +203,7 @@ Implementation of the 'generate' verb for HackMyResume.
var outFolder = PATH.dirname( f ); var outFolder = PATH.dirname( f );
MKDIRP.sync( outFolder ); // Ensure dest folder exists; MKDIRP.sync( outFolder ); // Ensure dest folder exists;
// JSON Resume themes have a 'render' method that needs to be called return theFormat.gen.generate( rez, f, _opts );
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( /@@@@~.*?~@@@@/gm, 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 ) {
@ -242,6 +213,27 @@ Implementation of the 'generate' verb for HackMyResume.
/**
Ensure that user-specified outputs/targets are valid.
*/
function verifyOutputs( targets, theme ) {
return _.reject(
targets.map( function( t ) {
var pathInfo = parsePath( t );
return {
format: pathInfo.extname.substr(1)
};
}),
function(t) {
return t.format === 'all' || theme.hasFormat( t.format );
}
);
}
/** /**
Expand output files. For example, "foo.all" should be expanded to Expand output files. For example, "foo.all" should be expanded to
["foo.html", "foo.doc", "foo.pdf", "etc"]. ["foo.html", "foo.doc", "foo.pdf", "etc"].
@ -301,10 +293,11 @@ Implementation of the 'generate' verb for HackMyResume.
} }
/** /**
Verify the specified theme name/path. Verify the specified theme name/path.
*/ */
function verify_theme( themeNameOrPath ) { function verifyTheme( themeNameOrPath ) {
var tFolder = PATH.join( var tFolder = PATH.join(
parsePath ( require.resolve('fresh-themes') ).dirname, parsePath ( require.resolve('fresh-themes') ).dirname,
'/themes/', '/themes/',
@ -323,9 +316,10 @@ Implementation of the 'generate' verb for HackMyResume.
/** /**
Load the specified theme. Load the specified theme, which could be either a FRESH theme or a JSON Resume
theme.
*/ */
function load_theme( tFolder ) { function loadTheme( tFolder ) {
// Create a FRESH or JRS theme object // Create a FRESH or JRS theme object
var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ? var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ?
@ -339,6 +333,21 @@ Implementation of the 'generate' verb for HackMyResume.
/**
Handle an exception. Placeholder.
*/
function error( ex ) {
throw ex;
}
function MDIN(txt) { // TODO: Move this
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
module.exports = build; module.exports = build;

View File

@ -1,3 +1,6 @@
/**
@module test-cli.js
*/
var chai = require('chai') var chai = require('chai')
, expect = chai.expect , expect = chai.expect
@ -6,7 +9,8 @@ var chai = require('chai')
, _ = require('underscore') , _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume') , FRESHResume = require('../src/core/fresh-resume')
, FCMD = require( '../src/hackmyapi') , FCMD = require( '../src/hackmyapi')
, validator = require('is-my-json-valid'); , validator = require('is-my-json-valid')
, EXTEND = require('../src/utils/extend');
chai.config.includeStack = false; chai.config.includeStack = false;
@ -14,9 +18,6 @@ describe('Testing CLI interface', function () {
var _sheet; var _sheet;
function logMsg() {
}
var opts = { var opts = {
format: 'FRESH', format: 'FRESH',
@ -31,46 +32,63 @@ describe('Testing CLI interface', function () {
silent: true silent: true
}; };
run( 'new', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' ); var sb = 'test/sandbox/';
run( 'new', ['test/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' ); var ft = 'node_modules/fresh-test-resumes/src/';
run( 'new', ['test/sandbox/new-1.json', 'test/sandbox/new-2.json', 'test/sandbox/new-3.json'], [], opts, ' (multiple FRESH 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)' );
fail( 'new', [], [], opts, " (when a filename isn't specified)" );
run( 'validate', ['node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json'], [], opts, ' (jane-q-fullstacker|FRESH)' ); [
run( 'validate', ['node_modules/fresh-test-resumes/src/johnny-trouble.fresh.json'], [], opts, ' (johnny-trouble|FRESH)' );
run( 'validate', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (new-fresh-resume|FRESH)' );
run( 'validate', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks.json|JRS)' );
run( 'validate', ['test/resumes/jrs-0.0.0/jane-incomplete.json'], [], opts2, ' (jane-incomplete.json|JRS)' );
run( 'validate', ['test/sandbox/new-1.json','test/sandbox/new-jrs-resume.json','test/sandbox/new-1.json', 'test/sandbox/new-2.json', 'test/sandbox/new-3.json'], [], opts, ' (5|BOTH)' );
run( 'analyze', ['node_modules/fresh-test-resumes/src/jane-fullstacker.json'], [], opts, ' (jane-q-fullstacker|FRESH)' ); [ 'new', [sb + 'new-fresh-resume.json'], [], opts, ' (FRESH format)' ],
run( 'analyze', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks|JRS)' ); [ 'new', [sb + 'new-jrs-resume.json'], [], opts2, ' (JRS format)'],
[ 'new', [sb + 'new-1.json', sb + 'new-2.json', sb + 'new-3.json'], [], opts, ' (multiple FRESH resumes)' ],
[ 'new', [sb + 'new-jrs-1.json', sb + 'new-jrs-2.json', sb + 'new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' ],
[ '!new', [], [], opts, " (when a filename isn't specified)" ],
[ 'validate', [ft + 'jane-fullstacker.fresh.json'], [], opts, ' (jane-q-fullstacker|FRESH)' ],
[ 'validate', [ft + 'johnny-trouble.fresh.json'], [], opts, ' (johnny-trouble|FRESH)' ],
[ 'validate', [sb + 'new-fresh-resume.json'], [], opts, ' (new-fresh-resume|FRESH)' ],
[ 'validate', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks.json|JRS)' ],
[ 'validate', ['test/resumes/jrs-0.0.0/jane-incomplete.json'], [], opts2, ' (jane-incomplete.json|JRS)' ],
[ 'validate', [sb + 'new-1.json', sb + 'new-jrs-resume.json', sb + 'new-1.json', sb + 'new-2.json', sb + 'new-3.json'], [], opts, ' (5|BOTH)' ],
[ 'analyze', [ft + 'jane-fullstacker.fresh.json'], [], opts, ' (jane-q-fullstacker|FRESH)' ],
[ 'analyze', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks|JRS)' ],
[ 'build', [ ft + 'jane-fullstacker.fresh.json', ft + 'override/jane-fullstacker-override.fresh.json' ], [ sb + 'merged/jane-fullstacker-gamedev.fresh.all'], opts, ' (jane-q-fullstacker w/ override|FRESH)' ],
[ '!build', [ ft + 'jane-fullstacker.fresh.json'], [ sb + 'shouldnt-exist.pdf' ], EXTEND(true, opts, { theme: 'awesome' }), ' (jane-q-fullstacker + Awesome + PDF|FRESH)' ]
].forEach( function(a) {
run.apply( /* The players of */ null, a );
});
run( 'build',
[ 'node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json',
'node_modules/fresh-test-resumes/src/override/jane-fullstacker-override.fresh.json' ],
[ 'test/sandbox/merged/jane-fullstacker-gamedev.fresh.all'], opts, ' (jane-q-fullstacker w/ override|FRESH)'
);
function run( verb, src, dst, opts, msg ) { function run( verb, src, dst, opts, msg ) {
msg = msg || '.'; msg = msg || '.';
it( 'The ' + verb.toUpperCase() + ' command should SUCCEED' + msg, function () { var shouldSucceed = true;
if( verb[0] === '!' ) {
verb = verb.substr(1);
shouldSucceed = false;
}
it( 'The ' + verb.toUpperCase() + ' command should ' + (shouldSucceed ? ' SUCCEED' : ' FAIL') + msg, function () {
function runIt() { function runIt() {
FCMD.verbs[verb]( src, dst, opts, opts.silent ? logMsg : function(msg){ msg = msg || ''; console.log(msg); } ); try {
FCMD.verbs[verb]( src, dst, opts, opts.silent ?
function(){} : function(msg){ msg = msg || ''; console.log(msg); } );
}
catch(ex) {
console.error(ex);
console.error(ex.stack);
throw ex;
}
} }
runIt.should.not.Throw(); if( shouldSucceed )
runIt.should.not.Throw();
else
runIt.should.Throw();
}); });
} }
function fail( verb, src, dst, opts, msg ) {
msg = msg || '.';
it( 'The ' + verb.toUpperCase() + ' command should FAIL' + msg, function () {
function runIt() {
FCMD.verbs[verb]( src, dst, opts, logMsg );
}
runIt.should.Throw();
});
}
}); });