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

Compare commits

...

38 Commits

Author SHA1 Message Date
6e5a44798b Update README. 2016-01-08 16:36:19 -05:00
1fbfe2507b Carry over debug flag. 2016-01-08 16:33:13 -05:00
d6a3aab68a Make Handlebars options explicit. 2016-01-08 16:27:19 -05:00
9fdfd1b5a6 Add baseline support for -d or --debug flag.
For now, -d just force-emits the stack when there is one. In the future,
it can trigger more detailed logging info.
2016-01-08 16:08:33 -05:00
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
34 changed files with 794 additions and 388 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:
```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`.
@ -90,8 +90,53 @@ hackmyresume BUILD resume.json -o myoptions.json
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:
`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,
YAML, print, smoke signal, carrier pigeon, and other arbitrary-format resumes
and CVs, from a single source of truth&mdash;without violating DRY.
2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats.
3. **Validate** resumes against either format.
2. **Analyze** your resume for keyword density, gaps/overlaps, and other
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,
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
```
Power users can install the latest bleeding-edge version (updated daily):
Alternately, install the latest bleeding-edge version (updated daily):
```bash
[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
@ -431,6 +442,16 @@ hackmyresume BUILD resume.json -o someFile.all -s
hackmyresume BUILD resume.json -o someFile.all --silent
```
### Debug Mode
Use `-d` or `--debug` to force HMR to emit a call stack when errors occur. In
the future, this option will emit detailed error logging.
```bash
hackmyresume BUILD resume.json -d
hackmyresume ANALYZE resume.json --debug
```
## Contributing
HackMyResume is a community-driven free and open source project under the MIT

View File

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

View File

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

View File

@ -3,7 +3,7 @@ Error-handling routines for HackMyResume.
@module error-handler.js
@license MIT. See LICENSE.md for details.
*/
// TODO: Logging library
(function() {
@ -15,6 +15,7 @@ Error-handling routines for HackMyResume.
, FS = require('fs')
, FCMD = require('../hackmyapi')
, PATH = require('path')
, WRAP = require('word-wrap')
, chalk = require('chalk');
@ -25,104 +26,149 @@ Error-handling routines for HackMyResume.
*/
var ErrorHandler = module.exports = {
init: function( debug ) {
this.debug = debug;
},
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( shouldExit )
process.exit( exitCode );
return;
}
// Get an error message -- either a HackMyResume error message or the
// exception's associated error message
if( ex.fluenterror ){
switch( ex.fluenterror ) {
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;
}
var errInfo = get_error_msg( ex );
msg = errInfo.msg;
exitCode = ex.fluenterror;
showStack = errInfo.showStack;
}
else {
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.
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
// Log non-HackMyResume-handled errors in red with ERROR prefix. Log HMR
// errors as-is.
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,
// output the error message and 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() );
}
// Selectively show the stack trace
if( (ex.stack || (ex.inner && ex.inner.stack)) &&
((showStack && ex.code !== 'ENOENT' ) || (this.debug) ))
log( chalk.red( ex.stack || ex.stack.inner ) );
// Let the error code be the process's return code.
if( shouldExit || ex.shouldExit )
process.exit( exitCode );
( shouldExit || ex.shouldExit ) && 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
sheets that have overlapping jobs.
*/
JRSResume.prototype.duration = function() {
JRSResume.prototype.duration = function( unit ) {
unit = unit || 'years';
if( this.work && this.work.length ) {
var careerStart = this.work[ this.work.length - 1].safeStartDate;
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
@ -283,7 +284,7 @@ Definition of the JRSResume class.
var careerLast = _.max( this.work, function( w ) {
return w.safeEndDate.unix();
}).safeEndDate;
return careerLast.diff( careerStart, 'years' );
return careerLast.diff( careerStart, unit );
}
return 0;
};

View File

@ -16,8 +16,7 @@ Definition of the JRSTheme class.
/**
The JRSTheme class is a representation of a JSON Resume
theme asset. See also: FRESHTheme.
The JRSTheme class is a representation of a JSON Resume theme asset.
@class JRSTheme
*/
function JRSTheme() {
@ -41,16 +40,40 @@ Definition of the JRSTheme class.
// 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.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 = {
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 {
throw { fluenterror: 10 };
throw { fluenterror: HACKMYSTATUS.missingPackageJSON };
}
return this;

View File

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

View File

@ -16,9 +16,11 @@ Status codes for HackMyResume.
resumeNotFoundAlt: 6,
inputOutputParity: 7,
createNameMissing: 8,
wkhtmltopdf: 9,
pdfgeneration: 9,
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, stack: ex.inner.stack } :
{ fluenterror: this.codes.pdfGeneration, inner: ex.inner, stack: ex.inner.stack };
}
}
});
// 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
*/
(function() {
var TemplateGenerator = require('./template-generator')
, FS = require('fs-extra')
, HTML = require( 'html' );
/**
An HTML-based PNG resume generator for HackMyResume.
*/
var HtmlPngGenerator = module.exports = TemplateGenerator.extend({
init: function() {
this._super( 'png', 'html' );
},
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 ) {
var htmlResults = opts.targets.filter(function(t){
return t.fmt.outFormat === 'html';
@ -30,18 +42,25 @@ Definition of the HtmlPngGenerator class.
var htmlFile = htmlResults[0].final.files.filter(function(fl){
return fl.info.ext === 'html';
});
png(htmlFile[0].data, f);
png( htmlFile[0].data, f );
}
});
/**
Generate a PNG from HTML.
*/
function png( markup, fOut ) {
// TODO: Which Webshot syntax?
// require('webshot')( markup , { encoding: 'binary', siteType: 'html' } )
// .pipe( FS.createWriteStream( fOut ) );
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.
@module template-generator.js
*/
@ -145,6 +145,10 @@ Definition of the TemplateGenerator class.
var thisFilePath;
if( theme.engine === 'jrs' ) {
file.info.orgPath = '';
}
if( file.info.action === 'transform' ) {
thisFilePath = PATH.join( outFolder, file.info.orgPath );
try {
@ -175,7 +179,8 @@ Definition of the TemplateGenerator class.
FS.copySync( file.info.path, thisFilePath );
}
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 ) {
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 );
this.opts.freezeBreaks && ( result = unfreeze(result) );
@ -274,8 +279,8 @@ Definition of the TemplateGenerator class.
theme );
}
catch(ex) {
console.log(ex);
throw ex;
ex.showStack = true;
require('../core/error-handler').err( ex );
}
}

View File

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

View File

@ -49,7 +49,8 @@ function main() {
.option('-o --opts <optionsFile>', 'Path to a .hackmyrc options file')
.option('-s --silent', 'Run in silent mode')
.option('--no-color', 'Disable colors')
.option('--color', 'Enable colors');
.option('--color', 'Enable colors')
.option('-d --debug', 'Enable diagnostics', false);
//.usage('COMMAND <sources> [TO <targets>]');
// Create the NEW command
@ -172,6 +173,7 @@ Invoke a HackMyResume verb.
*/
function execVerb( src, dst, opts, log ) {
loadOptions.call( this, opts );
require('./core/error-handler').init( _opts.debug );
HMR.verbs[ this.name() ].call( null, src, dst, _opts, log );
}
@ -180,23 +182,24 @@ function execVerb( src, dst, opts, log ) {
/**
Initialize HackMyResume options.
*/
function loadOptions( opts ) {
function loadOptions( o ) {
opts.opts = this.parent.opts;
o.opts = this.parent.opts;
// Load the specified options file (if any) and apply options
if( opts.opts && String.is( opts.opts )) {
var json = safeLoadJSON( PATH.relative( process.cwd(), opts.opts ) );
json && ( opts = EXTEND( true, opts, json ) );
if( o.opts && String.is( o.opts )) {
var json = safeLoadJSON( PATH.relative( process.cwd(), o.opts ) );
json && ( o = EXTEND( true, o, json ) );
if( !json ) {
throw safeLoadJSON.error;
}
}
// Merge in command-line options
opts = EXTEND( true, opts, this.opts() );
opts.silent = this.parent.silent;
_opts = opts;
o = EXTEND( true, o, this.opts() );
o.silent = this.parent.silent;
o.debug = this.parent.debug;
_opts = o;
}

View File

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

View File

@ -41,7 +41,7 @@ Definition of the HandlebarsGenerator class.
( format === 'doc' ) && (encData = json.xmlify());
// Compile and run the Handlebars template.
var template = HANDLEBARS.compile(jst);
var template = HANDLEBARS.compile(jst, { strict: false, assumeObjects: false });
return template({
r: encData,
RAW: json,
@ -61,30 +61,31 @@ Definition of the HandlebarsGenerator class.
function registerPartials(format, theme) {
if( format !== 'html' && format != 'doc' )
return;
if( format === 'html' || format === 'doc' ) {
// Locate the global partials folder
var partialsFolder = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/partials/',
format
);
// Locate the global partials folder
var partialsFolder = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/partials/',
format
);
// Register global partials in the /partials folder
// TODO: Only do this once per HMR invocation.
_.each( READFILES( partialsFolder, function(error){ }), function( el ) {
var pathInfo = parsePath( el );
var name = SLASH( PATH.relative( partialsFolder, el )
.replace(/\.html$|\.xml$/, '') );
if( pathInfo.dirname.endsWith('section') ) {
name = SLASH(name.replace(/\.html$|\.xml$/, ''));
}
var tplData = FS.readFileSync( el, 'utf8' );
var compiledTemplate = HANDLEBARS.compile( tplData );
HANDLEBARS.registerPartial( name, compiledTemplate );
theme.partialsInitialized = true;
});
// Register global partials in the /partials folder
// TODO: Only do this once per HMR invocation.
_.each( READFILES( partialsFolder, function(error){ }), function( el ) {
var pathInfo = parsePath( el );
var name = SLASH( PATH.relative( partialsFolder, el )
.replace(/\.html$|\.xml$/, '') );
if( pathInfo.dirname.endsWith('section') ) {
name = SLASH(name.replace(/\.html$|\.xml$/, ''));
}
var tplData = FS.readFileSync( el, 'utf8' );
var compiledTemplate = HANDLEBARS.compile( tplData );
HANDLEBARS.registerPartial( name, compiledTemplate );
theme.partialsInitialized = true;
});
}
// Register theme-specific partials
_.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')
, MKDIRP = require('mkdirp')
, EXTEND = require('../utils/extend')
, HACKMYSTATUS = require('../core/status-codes')
, parsePath = require('parse-filepath')
, _opts = require('../core/default-options')
, 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
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 ) {
// Housekeeping
//_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() );
}
prep( src, dst, opts, logger, errHandler );
// Load the theme...we do this first because the theme choice (FRESH or
// JSON Resume) determines what format we'll convert the resume to.
var tFolder = verify_theme( _opts.theme );
var theme = load_theme( tFolder );
var tFolder = verifyTheme( _opts.theme );
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...
if( !src || !src.length ) { throw { fluenterror: 3 }; }
@ -134,46 +115,73 @@ 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;
_opts.debug = opts.debug;
// 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".
TODO: Refactor.
@param targInfo Information for the target resume.
@param theme A FRESHTheme or JRSTheme object.
@returns
*/
function single( targInfo, theme, finished ) {
function MDIN(txt) { // TODO: Move this
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
try {
if( !targInfo.fmt ) {
return;
}
var f = targInfo.file
, fType = targInfo.fmt.outFormat
, fName = PATH.basename(f, '.' + fType)
, theFormat;
var suffix = '';
if( targInfo.fmt.outFormat === 'pdf' ) {
if( _opts.pdf ) {
if( _opts.pdf !== 'none' ) {
suffix = chalk.green(' (with ' + _opts.pdf + ')');
}
else {
_log( chalk.gray('Skipping ') +
chalk.white.bold(
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
chalk.gray(' resume') + suffix + chalk.green(': ') +
chalk.white( PATH.relative(process.cwd(), f )) );
return;
}
var suffix = '';
if( targInfo.fmt.outFormat === 'pdf' ) {
if( _opts.pdf ) {
if( _opts.pdf !== 'none' ) {
suffix = chalk.green(' (with ' + _opts.pdf + ')');
}
else {
_log( chalk.gray('Skipping ') +
chalk.white.bold(
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
chalk.gray(' resume') + suffix + chalk.green(': ') +
chalk.white( PATH.relative(process.cwd(), f )) );
return;
}
}
}
_log( chalk.green('Generating ') +
chalk.green.bold(
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
chalk.green(' resume') + suffix + chalk.green(': ') +
chalk.green.bold( PATH.relative(process.cwd(), f )) );
_log( chalk.green('Generating ') +
chalk.green.bold(
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
chalk.green(' resume') + suffix + chalk.green(': ') +
chalk.green.bold( PATH.relative(process.cwd(), f )) );
// If targInfo.fmt.files exists, this format is backed by a document.
// Fluent/FRESH themes are handled here.
@ -185,8 +193,8 @@ Implementation of the 'generate' verb for HackMyResume.
return theFormat.gen.generate( rez, f, _opts );
}
// Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format
// (JSON, YML, or PNG) that every theme gets "for free".
//Otherwise this is an ad-hoc format (JSON, YML, or PNG) that every theme
// gets "for free".
else {
theFormat = _fmts.filter( function(fmt) {
@ -196,43 +204,7 @@ Implementation of the 'generate' verb for HackMyResume.
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( /@@@@~.*?~@@@@/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 );
}
return theFormat.gen.generate( rez, f, _opts );
}
}
catch( ex ) {
@ -242,6 +214,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
["foo.html", "foo.doc", "foo.pdf", "etc"].
@ -301,10 +294,11 @@ Implementation of the 'generate' verb for HackMyResume.
}
/**
Verify the specified theme name/path.
*/
function verify_theme( themeNameOrPath ) {
function verifyTheme( themeNameOrPath ) {
var tFolder = PATH.join(
parsePath ( require.resolve('fresh-themes') ).dirname,
'/themes/',
@ -323,9 +317,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
var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ?
@ -339,6 +334,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;

View File

@ -1,3 +1,6 @@
/**
@module test-cli.js
*/
var chai = require('chai')
, expect = chai.expect
@ -6,7 +9,8 @@ var chai = require('chai')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, 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;
@ -14,9 +18,6 @@ describe('Testing CLI interface', function () {
var _sheet;
function logMsg() {
}
var opts = {
format: 'FRESH',
@ -31,46 +32,63 @@ describe('Testing CLI interface', function () {
silent: true
};
run( 'new', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
run( 'new', ['test/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
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)" );
var sb = 'test/sandbox/';
var ft = 'node_modules/fresh-test-resumes/src/';
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)' );
run( 'analyze', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks|JRS)' );
[ 'new', [sb + 'new-fresh-resume.json'], [], opts, ' (FRESH format)' ],
[ '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 ) {
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() {
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();
});
}
});