1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-12-27 07:17:23 +00:00

Revamp command-line functionality.

Get resume generation CLI up and running again after recent API updates.
Tool name has changed to "fluentcmd" from "scrappy" and now depends on
external "fluentlib" instead of embedding those sources.
This commit is contained in:
devlinjd 2015-09-24 16:09:48 -04:00
parent 1715a66514
commit 76f718dc3e
14 changed files with 195 additions and 1167 deletions

View File

@ -1,6 +1,6 @@
scrappy
=======
The original Node.js-based proof-of-concept command line tool for **FluentCV**.
fluentcmd
=========
The command-line FluentCV application for Linux, Windows, and OS X.
## Use
@ -9,23 +9,23 @@ First make sure [Node.js][4] and [NPM][5] are installed. Then:
1. Install the latest official [PhantomJS][2] and [wkhtmltopdf][3] binaries for your platform.
2. Verify PhantomJS and wkhtml are accessible on your path.
3. Run `npm install` followed by `npm link`.
4. Run Scrappy from with `scrappy [input] [output] -t [theme]`. For example:
4. Run fluentcmd from with `fluentcmd [input] [output] -t [theme]`. For example:
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT)
scrappy resume.json resume.all -t informatic
fluentcmd resume.json resume.all -t informatic
# Generate a specific resume format
scrappy resume.json resume.html -t informatic
scrappy resume.json resume.txt -t informatic
scrappy resume.json resume.pdf -t informatic
scrappy resume.json resume.doc -t informatic
fluentcmd resume.json resume.html -t informatic
fluentcmd resume.json resume.txt -t informatic
fluentcmd resume.json resume.pdf -t informatic
fluentcmd resume.json resume.doc -t informatic
```
5. Success looks like this:
```
*** Scrappy v0.1.0 ***
*** FluentCMD v0.1.0 ***
Reading JSON resume: foo/resume.json
Generating HTML resume: out/resume.html
Generating TXT resume: out/resume.txt
@ -39,21 +39,21 @@ You can **merge multiple resumes** by specifying them in order from most generic
```bash
# Merge specific.json onto base.json and generate all formats
scrappy base.json specific.json resume.all -t informatic
fluentcmd base.json specific.json resume.all -t informatic
```
You can specify **multiple output filenames** instead of using `.all`:
```bash
# Merge specific.json onto base.json and generate r1.doc and r2.pdf
scrappy base.json specific.json r1.doc r2.pdf -t informatic
fluentcmd base.json specific.json r1.doc r2.pdf -t informatic
```
You can omit the output file(s) and/or theme completely:
```bash
# Equivalent to "scrappy resume.json resume.all -t default"
scrappy resume.json
# Equivalent to "fluentcmd resume.json resume.all -t default"
fluentcmd resume.json
```
## License

View File

@ -1,11 +1,11 @@
{
"name": "scrappy",
"name": "fluentcmd",
"version": "0.1.0",
"description": "An extensible command-line-based resume generator for the 21st century.",
"description": "The FluentCV command-line tool (an extensible command-line-based resume generator for the 21st century.)",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/gruebait/scrappy.git"
"url": "https://github.com/fluentcv/fluentcmd.git"
},
"keywords": [
"resume",
@ -17,11 +17,14 @@
"license": "UNLICENSED",
"preferGlobal": "true",
"bugs": {
"url": "https://github.com/gruebait/scrappy/issues"
"url": "https://github.com/fluentcv/fluentcmd/issues"
},
"main": "src/scrappy.js",
"homepage": "https://github.com/gruebait/scrappy",
"bin": {
"fluentcmd": "src/index.js"
},
"homepage": "https://github.com/fluentcv/fluentcmd",
"dependencies": {
"fluentlib": "file:..\\fluentlib",
"fs-extra": "^0.24.0",
"html": "0.0.10",
"is-my-json-valid": "^2.12.2",

168
src/fluentcmd.js Normal file
View File

@ -0,0 +1,168 @@
/**
Core resume generation module for FluentCMD.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
module.exports = function () {
var MD = require( 'marked' )
, XML = require( 'xml-escape' )
, HTML = require( 'html' )
, FS = require( 'fs-extra' )
, XML = require( 'xml-escape' )
, path = require( 'path' )
, extend = require( './utils/extend' )
, _ = require('underscore')
, FLUENT = require('fluentlib');
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
var rez;
/**
Core resume generation method for HMR. Given a source JSON resume file, a
destination resume spec, and a theme file, generate 0..N resumes in the
requested formats. Requires filesystem access. To perform generation without
filesystem access, use the single() method below.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst Path to the destination resume file(s): "rez/resume.all".
@param theme Friendly name of the resume theme. Defaults to "default".
*/
function hmr( src, dst, theme ) {
_opts.theme = theme;
dst = (dst && dst.length && dst) || ['resume.all'];
// console.log( src );
// console.log( dst );
// console.log( theme );
// Assemble output resume targets
var targets = [];
dst.forEach( function(t) {
t = path.resolve(t);
var dot = t.lastIndexOf('.');
var format = ( dot === -1 ) ? 'all' : t.substring( dot + 1 );
var temp = ( format === 'all' ) ?
_fmts.map( function( fmt ) { return t.replace( /all$/g, fmt.name ); }) :
( format === 'doc' ? [ 'doc' ] : [ t ] ); // interim code
targets.push.apply(targets, temp);
});
// Assemble input resumes
var sheets = src.map( function( res ) {
console.log( 'Reading JSON resume: ' + res );
return (new FLUENT.Sheet()).open( res );
});
// Merge input resumes
rez = sheets.reduce( function( acc, elem ) {
return extend( true, acc.rep, elem.rep );
});
// Run the transformation!
var finished = targets.map( gen );
return {
sheet: rez,//.rep,
targets: targets,
processed: finished
};
}
/**
Generate a single resume of a specific format.
@param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function gen( f ) {
try {
// Get the output file type (pdf, html, txt, etc)
var fType = path.extname( f ).trim().toLowerCase().substr(1);
var fName = path.basename( f, '.' + fType );
// Get the format object (if any) corresponding to that type, and assemble
// the final output file path for the generated resume.
var fObj = _fmts.filter( function(_f) { return _f.name === fType; } )[0];
var fOut = path.join( f.substring( 0, f.lastIndexOf('.') + 1 ) + fObj.ext );
// Generate!
console.log( 'Generating ' + fType.toUpperCase() + ' resume: ' + fOut );
return fObj.gen.generate( rez, fOut, _opts.theme );
}
catch( ex ) {
err( ex );
}
}
/**
Handle an exception.
*/
function err( ex ) {
var msg = ex.toString();
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
console.error( 'ERROR: ' + trimmed.toString() );
}
/**
Supported resume formats.
*/
var _fmts = [
{ name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() },
{ name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() }
];
/**
Default options.
*/
var _opts = {
prettyPrint: true,
prettyIndent: 2,
keepBreaks: true,
nSym: '&newl;',
rSym: '&retn;',
theme: 'default',
sheets: [],
filters: {
out: function( txt ) { return txt; },
raw: function( txt ) { return txt; },
xml: function( txt ) { return XML(txt); },
md: function( txt ) { return MD(txt); },
mdin: function( txt ) { return MD(txt).replace(/^\s*\<p\>|\<\/p\>\s*$/gi, ''); },
lower: function( txt ) { return txt.toLowerCase(); }
},
template: {
interpolate: /\{\{(.+?)\}\}/g,
escape: /\{\{\=(.+?)\}\}/g,
evaluate: /\{\%(.+?)\%\}/g,
comment: /\{\#(.+?)\#\}/g
},
pdf: 'wkhtmltopdf'
}
/**
Regexes for linebreak preservation.
*/
var _reg = {
regN: new RegExp( '\n', 'g' ),
regR: new RegExp( '\r', 'g' ),
regSymN: new RegExp( _opts.nSym, 'g' ),
regSymR: new RegExp( _opts.rSym, 'g' )
};
/**
Module public interface.
*/
return {
generate: hmr,
options: _opts,
formats: _fmts
};
}();

View File

@ -1,18 +0,0 @@
/**
Base resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
(function() {
var Class = require( '../utils/class' );
/**
The BaseGenerator class is the root of the generator hierarchy. Functionality
common to ALL generators lives here.
*/
var BaseGenerator = module.exports = Class.extend({
init: function( outputFormat ) {
this.format = outputFormat;
}
});
}());

View File

@ -1,31 +0,0 @@
/**
HTML resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
var TemplateGenerator = require('./template-generator');
var FS = require('fs-extra');
var HTML = require( 'html' );
var HtmlGenerator = TemplateGenerator.extend({
init: function() {
this._super( 'html' );
},
/**
Generate an HTML resume with optional pretty printing.
*/
onBeforeSave: function( mk, themeFile, outputFile ) {
var cssSrc = themeFile.replace( /.html$/g, '.css' );
var cssDst = outputFile.replace( /.html$/g, '.css' );
FS.copy( cssSrc, cssDst, function( e ) {
if( e ) err( "Couldn't copy CSS file to destination: " + err);
});
return true ?
HTML.prettyPrint( mk, { indent_size: 2 } ) : mk;
}
});
module.exports = HtmlGenerator;

View File

@ -1,31 +0,0 @@
/**
HTML-based PDF resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
var TemplateGenerator = require('./template-generator');
var FS = require('fs-extra');
var HTML = require( 'html' );
var HtmlPdfGenerator = TemplateGenerator.extend({
init: function() {
this._super( 'pdf', 'html' );
},
/**
Generate an HTML resume with optional pretty printing.
*/
onBeforeSave: function( mk, themeFile, outputFile ) {
var cssSrc = themeFile.replace( /.html$/g, '.css' );
var cssDst = outputFile.replace( /.html$/g, '.css' );
FS.copy( cssSrc, cssDst, function( e ) {
if( e ) err( "Couldn't copy CSS file to destination: " + err);
});
return true ?
HTML.prettyPrint( mk, { indent_size: 2 } ) : mk;
}
});
module.exports = HtmlPdfGenerator;

View File

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

View File

@ -1,153 +0,0 @@
/**
Template-based resume generator base for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
(function() {
var FS = require( 'fs' );
var BaseGenerator = require( './base-generator' );
var _ = require( 'underscore' );
var MD = require( 'marked' );
var XML = require( 'xml-escape' );
var path = require('path');
var _opts = {
keepBreaks: true,
nSym: '&newl;',
rSym: '&retn;',
template: {
interpolate: /\{\{(.+?)\}\}/g,
escape: /\{\{\=(.+?)\}\}/g,
evaluate: /\{\%(.+?)\%\}/g,
comment: /\{\#(.+?)\#\}/g
},
filters: {
out: function( txt ) { return txt; },
raw: function( txt ) { return txt; },
xml: function( txt ) { return XML(txt); },
md: function( txt ) { return MD(txt); },
mdin: function( txt ) { return MD(txt).replace(/^\s*\<p\>|\<\/p\>\s*$/gi, ''); },
lower: function( txt ) { return txt.toLowerCase(); }
},
prettyPrint: true,
prettyIndent: 2
};
/**
TemplateGenerator performs resume generation via Underscore-style template
expansion and is appropriate for text-based formats like HTML, plain text,
and XML versions of Microsoft Word, Excel, and OpenOffice.
*/
var TemplateGenerator = BaseGenerator.extend({
/** outputFormat: html, txt, pdf, doc
templateFormat: html or txt
**/
init: function( outputFormat, templateFormat, cssFile ){
this._super( outputFormat );
this.tplFormat = templateFormat || outputFormat;
},
/** Default generation method for template-based generators. */
generate: function( rez, f, themeName ) {
try {
// Get the output file type (pdf, html, txt, etc)
var fName = path.basename( f, '.' + this.format );
// Load the active theme file, including CSS data if req'd
var themeFile = path.join( __dirname, '../../../watermark/', themeName, this.format + '.' + this.tplFormat );
var cssData = this.tplFormat === 'html' ? FS.readFileSync( path.join( __dirname, '../../../watermark/', themeName, 'html.css' ), 'utf8' ) : null;
var mk = FS.readFileSync( themeFile, 'utf8' );
// Compile and invoke the template!
mk = this.single( rez, mk, this.format, cssData, fName );
this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f ));
// Post-process and save the file
FS.writeFileSync( f, mk, 'utf8' );
return mk;
}
catch( ex ) {
err( ex );
}
},
/**
Perform a single resume JSON-to-DEST resume transformation. Exists as a
separate function in order to expose string-based transformations to clients
who don't have access to filesystem resources (in-browser, etc.).
*/
single: function( json, jst, format, styles, fName ) {
// Freeze whitespace in the template
_opts.keepBreaks && ( jst = freeze(jst) );
// Tweak underscore's default template delimeters
_.templateSettings = _opts.template;
// Convert {{ someVar }} to {% print(filt.out(someVar) %}
// Convert {{ someVar|someFilter }} to {% print(filt.someFilter(someVar) %}
jst = jst.replace( _.templateSettings.interpolate, function replace(m, p1) {
if( p1.indexOf('|') > -1 ) {
var terms = p1.split('|');
return '{% print( filt.' + terms[1] + '( ' + terms[0] + ' )) %}';
}
else {
return '{% print( filt.out(' + p1 + ') ) %}';
}
});
// Strip {# comments #}
jst = jst.replace( _.templateSettings.comment, '');
json.display_progress_bar = true;
// Compile and run the template. TODO: avoid unnecessary recompiles.
jst = _.template( jst )({ r: json, css: styles, embedCss: false, cssFile: fName, filt: _opts.filters });
// Unfreeze whitespace
_opts.keepBreaks && ( jst = unfreeze(jst) );
return jst;
}
});
/**
Export the TemplateGenerator function/ctor.
*/
module.exports = TemplateGenerator;
/**
Freeze newlines for protection against errant JST parsers.
*/
function freeze( markup ) {
return markup
.replace( _reg.regN, _opts.nSym )
.replace( _reg.regR, _opts.rSym );
}
/**
Unfreeze newlines when the coast is clear.
*/
function unfreeze( markup ) {
return markup
.replace( _reg.regSymR, '\r' )
.replace( _reg.regSymN, '\n' );
}
/**
Regexes for linebreak preservation.
*/
var _reg = {
regN: new RegExp( '\n', 'g' ),
regR: new RegExp( '\r', 'g' ),
regSymN: new RegExp( _opts.nSym, 'g' ),
regSymR: new RegExp( _opts.rSym, 'g' )
};
}());

View File

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

View File

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

View File

@ -1,22 +1,22 @@
#! /usr/bin/env node
/**
Command-line resume generation logic for Scrappy.
Command-line resume generation logic for FluentCMD.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
var ARGS = require( 'minimist' )
, HMR = require( './scrappy');
, FCMD = require( './fluentcmd');
try {
console.log( '*** Scrappy v0.1.0 ***' );
console.log( '*** FluentCMD v0.1.0 ***' );
if( process.argv.length <= 2 ) { throw 'Please specify a JSON resume file.'; }
var args = ARGS( process.argv.slice(2) );
var src = args._.filter( function( a ) { return a.endsWith('.json'); });
var dst = args._.filter( function( a ) { return !a.endsWith('.json'); });
HMR.generate( src, dst, args.t || 'default' );
FCMD.generate( src, dst, args.t || 'default' );
}
catch( ex ) {

View File

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

View File

@ -1,296 +0,0 @@
/**
Core resume generation module for Scrappy.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
module.exports = function () {
var MD = require( 'marked' )
, XML = require( 'xml-escape' )
, HTML = require( 'html' )
, FS = require( 'fs-extra' )
, XML = require( 'xml-escape' )
, path = require( 'path' )
, extend = require( './utils/extend' )
, _ = require('underscore')
, Sheet = require('./sheet')
, HtmlGenerator = require('./gen/html-generator')
, TextGenerator = require('./gen/text-generator')
, HtmlPdfGenerator = require('./gen/html-pdf-generator')
, WordGenerator = require('./gen/word-generator');
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
var rez;
/**
Core resume generation method for HMR. Given a source JSON resume file, a
destination resume spec, and a theme file, generate 0..N resumes in the
requested formats. Requires filesystem access. To perform generation without
filesystem access, use the single() method below.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst Path to the destination resume file(s): "rez/resume.all".
@param theme Friendly name of the resume theme. Defaults to "default".
*/
function hmr( src, dst, theme ) {
_opts.theme = theme;
dst = (dst && dst.length && dst) || ['resume.all'];
// Assemble output resume targets
var targets = [];
dst.forEach( function(t) {
var dot = t.lastIndexOf('.');
var format = ( dot === -1 ) ? 'all' : t.substring( dot + 1 );
var temp = ( format === 'all' ) ?
_fmts.map( function( fmt ) { return t.replace( /all$/g, fmt.name ); }) :
( format === 'doc' ? [ 'doc' ] : [ t ] ); // interim code
targets.push.apply(targets, temp);
});
// Assemble input resumes
var sheets = src.map( function( res ) {
console.log( 'Reading JSON resume: ' + res );
return (new Sheet()).open( res );
});
// Merge input resumes
rez = sheets.reduce( function( acc, elem ) {
return extend( true, acc.rep, elem.rep );
});
// Run the transformation!
var finished = targets.map( gen );
return {
sheet: rez,//.rep,
targets: targets,
processed: finished
};
}
/**
Generate a single resume of a specific format.
@param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function gen( f ) {
try {
// Get the output file type (pdf, html, txt, etc)
var fType = path.extname( f ).trim().toLowerCase().substr(1);
var fName = path.basename( f, '.' + fType );
// Get the format object (if any) corresponding to that type, and assemble
// the final output file path for the generated resume.
var fObj = _fmts.filter( function(_f) { return _f.name === fType; } )[0];
var fOut = path.join( f.substring( 0, f.lastIndexOf('.') + 1 ) + fObj.ext );
console.log( 'Generating ' + fType.toUpperCase() + ' resume: ' + fOut );
// Load the active theme file, including CSS data if req'd
var themeFile = path.join( __dirname, '../../watermark/', _opts.theme,
fType + '.' + (fObj.fmt || fObj.ext));
var cssData = (fType !== 'html' && fType !== 'pdf') ? null :
FS.readFileSync( path.join( __dirname, '../../watermark/', _opts.theme, 'html.css' ), 'utf8' );
var mk = FS.readFileSync( themeFile, 'utf8' );
// Compile and invoke the template!
mk = single( rez, mk, fType, cssData, fName );
// Post-process and save the file
fType === 'html' && (mk = html( mk, themeFile, fOut ));
fType === 'pdf' && pdf( mk, fOut );
fType !== 'pdf' && FS.writeFileSync( fOut, mk, 'utf8' );
return mk;
}
catch( ex ) {
err( ex );
}
}
/**
Perform a single resume JSON-to-DEST resume transformation. Exists as a
separate function in order to expose string-based transformations to clients
who don't have access to filesystem resources (in-browser, etc.).
*/
function single( json, jst, format, styles, fName ) {
// Freeze whitespace in the template
_opts.keepBreaks && ( jst = freeze(jst) );
// Tweak underscore's default template delimeters
_.templateSettings = _opts.template;
// Convert {{ someVar }} to {% print(filt.out(someVar) %}
// Convert {{ someVar|someFilter }} to {% print(filt.someFilter(someVar) %}
jst = jst.replace( _.templateSettings.interpolate, function replace(m, p1) {
if( p1.indexOf('|') > -1 ) {
var terms = p1.split('|');
return '{% print( filt.' + terms[1] + '( ' + terms[0] + ' )) %}';
}
else {
return '{% print( filt.out(' + p1 + ') ) %}';
}
});
// Strip {# comments #}
jst = jst.replace( _.templateSettings.comment, '');
json.display_progress_bar = true;
// Compile and run the template. TODO: avoid unnecessary recompiles.
jst = _.template( jst )({ r: json, css: styles, embedCss: false, cssFile: fName, filt: _opts.filters });
// Unfreeze whitespace
_opts.keepBreaks && ( jst = unfreeze(jst) );
return jst;
}
/**
Handle an exception.
*/
function err( ex ) {
var msg = ex.toString();
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
console.error( 'ERROR: ' + trimmed.toString() );
}
/**
Generate an HTML resume with optional pretty printing.
*/
function html( mk, themeFile, outputFile ) {
var cssSrc = themeFile.replace( /.html$/g, '.css' );
var cssDst = outputFile.replace( /.html$/g, '.css' );
FS.copy( cssSrc, cssDst, function( e ) {
if( e ) err( "Couldn't copy CSS file to destination: " + err);
});
return _opts.prettyPrint ? // TODO: copy CSS
HTML.prettyPrint( mk, { indent_size: _opts.prettyIndent } ) : mk;
}
/**
Generate a PDF from HTML.
*/
function pdf( markup, fOut ) {
var pdfCount = 0;
if( _opts.pdf === 'phantom' || _opts.pdf == 'all' ) {
pdfCount++;
require('phantom').create( function( ph ) {
ph.createPage( function( page ) {
page.setContent( markup );
page.set('paperSize', {
format: 'A4',
orientation: 'portrait',
margin: '1cm'
});
page.set("viewportSize", {
width: 1024, // TODO: option-ify
height: 768 // TODO: Use "A" sizes
});
page.set('onLoadFinished', function(success) {
page.render( fOut );
pdfCount++;
ph.exit();
});
},
{ dnodeOpts: { weak: false } } );
});
}
if( _opts.pdf === 'wkhtmltopdf' || _opts.pdf == 'all' ) {
var fOut2 = fOut;
if( pdfCount == 1 ) {
fOut2 = fOut2.replace(/\.pdf$/g, '.b.pdf');
}
require('wkhtmltopdf')( markup, { pageSize: 'letter' } )
.pipe( FS.createWriteStream( fOut2 ) );
pdfCount++;
}
}
/**
Freeze newlines for protection against errant JST parsers.
*/
function freeze( markup ) {
return markup
.replace( _reg.regN, _opts.nSym )
.replace( _reg.regR, _opts.rSym );
}
/**
Unfreeze newlines when the coast is clear.
*/
function unfreeze( markup ) {
return markup
.replace( _reg.regSymR, '\r' )
.replace( _reg.regSymN, '\n' );
}
/**
Supported resume formats.
*/
var _fmts = [
{ name: 'html', ext: 'html' },
{ name: 'txt', ext: 'txt' },
{ name: 'doc', ext: 'doc', fmt: 'xml' },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false }
];
/**
Default options.
*/
var _opts = {
prettyPrint: true,
prettyIndent: 2,
keepBreaks: true,
nSym: '&newl;',
rSym: '&retn;',
theme: 'default',
sheets: [],
filters: {
out: function( txt ) { return txt; },
raw: function( txt ) { return txt; },
xml: function( txt ) { return XML(txt); },
md: function( txt ) { return MD(txt); },
mdin: function( txt ) { return MD(txt).replace(/^\s*\<p\>|\<\/p\>\s*$/gi, ''); },
lower: function( txt ) { return txt.toLowerCase(); }
},
template: {
interpolate: /\{\{(.+?)\}\}/g,
escape: /\{\{\=(.+?)\}\}/g,
evaluate: /\{\%(.+?)\%\}/g,
comment: /\{\#(.+?)\#\}/g
},
pdf: 'wkhtmltopdf'
}
/**
Regexes for linebreak preservation.
*/
var _reg = {
regN: new RegExp( '\n', 'g' ),
regR: new RegExp( '\r', 'g' ),
regSymN: new RegExp( _opts.nSym, 'g' ),
regSymR: new RegExp( _opts.rSym, 'g' )
};
/**
Module public interface.
*/
return {
generate: hmr,
transform: single,
options: _opts,
formats: _fmts,
Sheet: Sheet,
HtmlGenerator: HtmlGenerator,
TextGenerator: TextGenerator,
HtmlPdfGenerator: HtmlPdfGenerator,
WordGenerator: WordGenerator
};
}();

View File

@ -1,185 +0,0 @@
/**
Abstract character/resume sheet representation.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
*/
(function() {
var FS = require('fs')
, extend = require('./utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, moment = require('moment');
/**
The Sheet class represent a specific JSON character sheet. When Sheet.open
is called, we merge the loaded JSON sheet properties onto the Sheet instance
via extend(), so a full-grown sheet object will have all of the methods here,
plus a complement of JSON properties from the backing JSON file. That allows
us to treat Sheet objects interchangeably with the loaded JSON model.
@class Sheet
*/
function Sheet() {
this.meta = { };
}
/**
Open and parse the specified JSON resume sheet. Validate any dates present in
the sheet and convert them to a safe/consistent format. Then sort each section
on the sheet by startDate descending.
*/
Sheet.prototype.open = function( file, title ) {
var rep = JSON.parse( FS.readFileSync( file, 'utf8' ) );
extend( true, this, rep );
this.meta.fileName = file;
this.meta.title = title || this.basics.name;
_parseDates.call( this );
this.sort();
this.computed = this.computed || { };
this.computed.numYears = this.duration();
return this;
};
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
Sheet.prototype.hasProfile = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.basics.profiles && _.some( this.basics.profiles, function(p) {
return p.network.trim().toLowerCase() === socialNetwork;
});
};
/**
Determine if the sheet includes a specific skill.
*/
Sheet.prototype.hasSkill = function( skill ) {
skill = skill.trim().toLowerCase();
return this.skills && _.some( this.skills, function(sk) {
return sk.keywords && _.some( sk.keywords, function(kw) {
return kw.trim().toLowerCase() === skill;
});
});
};
/**
Validate the sheet against the JSON Resume schema.
*/
Sheet.prototype.isValid = function( ) {
var schema = FS.readFileSync( __dirname + '/resume-schema.json', 'utf8' );
var schemaObj = JSON.parse( schema );
var validator = require('is-my-json-valid')
var validate = validator( schemaObj );
return validate( this );
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
Sheet.prototype.duration = function() {
var careerStart = this.work[ this.work.length - 1].safeStartDate;
var careerLast = _.max( this.work, function( w ) {
return w.safeEndDate.unix();
}).safeEndDate;
return careerLast.diff( careerStart, 'years' );
};
/**
Sort dated things on the sheet by start date descending.
*/
Sheet.prototype.sort = function( ) {
this.work && this.work.sort( byDateDesc );
this.education && this.education.sort( byDateDesc );
this.volunteer && this.volunteer.sort( byDateDesc );
this.awards && this.awards.sort( function(a, b) {
return( a.safeDate.isBefore(b.safeDate) ) ? 1
: ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
});
this.publications && this.publications.sort( function(a, b) {
return( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) ? 1
: ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0;
});
function byDateDesc(a,b) {
return( a.safeStartDate.isBefore(b.safeStartDate) ) ? 1
: ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0;
}
};
/**
Format a human-friendly FluentCV date to a Moment.js-compatible date. There
are a few date formats to be aware of here.
- The words "Present" and "Now", referring to the current date.
- The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10").
- The friendly FluentCV "mmm YYYY" format ("Mar 2015" or "Dec 2008").
- Year-only "YYYY" ("2015").
- Any other date format that Moment.js can parse from.
*/
function _fmt( dt ) {
dt = dt.toLowerCase().trim();
if( /\s*(present|now)\s*/i.test(dt) ) { // "Present", "Now"
return moment();
}
else if( /^\D+/.test(dt) ) { // "Mar 2015"
var parts = dt.split(' ');
var dt = parts[1] + '-' + (months[parts[0]] || abbr[parts[0]]) + '-01';
return moment( dt, 'YYYY-MM-DD' );
}
else if( /^\d+$/.test(dt) ) { // "2015"
return moment( dt, 'YYYY' );
}
else {
var mt = moment( dt );
if(mt.isValid())
return mt;
throw 'Invalid date format encountered. Use YYYY-MM-DD.';
}
}
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
function _parseDates() {
this.work.forEach( function(job) {
job.safeStartDate = _fmt( job.startDate );
job.safeEndDate = _fmt( job.endDate );
});
this.education.forEach( function(edu) {
edu.safeStartDate = _fmt( edu.startDate );
edu.safeEndDate = _fmt( edu.endDate );
});
this.volunteer.forEach( function(vol) {
vol.safeStartDate = _fmt( vol.startDate );
vol.safeEndDate = _fmt( vol.endDate );
});
this.awards.forEach( function(awd) {
awd.safeDate = _fmt( awd.date );
});
this.publications.forEach( function(pub) {
pub.safeReleaseDate = _fmt( pub.releaseDate );
});
}
var months = {}, abbr = {};
moment.months().forEach(function(m,idx){months[m.toLowerCase()]=idx+1;});
moment.monthsShort().forEach(function(m,idx){abbr[m.toLowerCase()]=idx+1;});
abbr.sept = 9;
/**
Export the Sheet function/ctor.
*/
module.exports = Sheet;
}());