mirror of
https://github.com/JuanCanham/HackMyResume.git
synced 2024-11-05 01:56:21 +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:
parent
1715a66514
commit
76f718dc3e
28
README.md
28
README.md
@ -1,6 +1,6 @@
|
|||||||
scrappy
|
fluentcmd
|
||||||
=======
|
=========
|
||||||
The original Node.js-based proof-of-concept command line tool for **FluentCV**.
|
The command-line FluentCV application for Linux, Windows, and OS X.
|
||||||
|
|
||||||
## Use
|
## 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.
|
1. Install the latest official [PhantomJS][2] and [wkhtmltopdf][3] binaries for your platform.
|
||||||
2. Verify PhantomJS and wkhtml are accessible on your path.
|
2. Verify PhantomJS and wkhtml are accessible on your path.
|
||||||
3. Run `npm install` followed by `npm link`.
|
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
|
```bash
|
||||||
# Generate all resume formats (HTML, PDF, DOC, TXT)
|
# 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
|
# Generate a specific resume format
|
||||||
scrappy resume.json resume.html -t informatic
|
fluentcmd resume.json resume.html -t informatic
|
||||||
scrappy resume.json resume.txt -t informatic
|
fluentcmd resume.json resume.txt -t informatic
|
||||||
scrappy resume.json resume.pdf -t informatic
|
fluentcmd resume.json resume.pdf -t informatic
|
||||||
scrappy resume.json resume.doc -t informatic
|
fluentcmd resume.json resume.doc -t informatic
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Success looks like this:
|
5. Success looks like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
*** Scrappy v0.1.0 ***
|
*** FluentCMD v0.1.0 ***
|
||||||
Reading JSON resume: foo/resume.json
|
Reading JSON resume: foo/resume.json
|
||||||
Generating HTML resume: out/resume.html
|
Generating HTML resume: out/resume.html
|
||||||
Generating TXT resume: out/resume.txt
|
Generating TXT resume: out/resume.txt
|
||||||
@ -39,21 +39,21 @@ You can **merge multiple resumes** by specifying them in order from most generic
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Merge specific.json onto base.json and generate all formats
|
# 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`:
|
You can specify **multiple output filenames** instead of using `.all`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Merge specific.json onto base.json and generate r1.doc and r2.pdf
|
# 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:
|
You can omit the output file(s) and/or theme completely:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Equivalent to "scrappy resume.json resume.all -t default"
|
# Equivalent to "fluentcmd resume.json resume.all -t default"
|
||||||
scrappy resume.json
|
fluentcmd resume.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
15
package.json
15
package.json
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "scrappy",
|
"name": "fluentcmd",
|
||||||
"version": "0.1.0",
|
"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,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/gruebait/scrappy.git"
|
"url": "https://github.com/fluentcv/fluentcmd.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"resume",
|
"resume",
|
||||||
@ -17,11 +17,14 @@
|
|||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"preferGlobal": "true",
|
"preferGlobal": "true",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/gruebait/scrappy/issues"
|
"url": "https://github.com/fluentcv/fluentcmd/issues"
|
||||||
},
|
},
|
||||||
"main": "src/scrappy.js",
|
"bin": {
|
||||||
"homepage": "https://github.com/gruebait/scrappy",
|
"fluentcmd": "src/index.js"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/fluentcv/fluentcmd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"fluentlib": "file:..\\fluentlib",
|
||||||
"fs-extra": "^0.24.0",
|
"fs-extra": "^0.24.0",
|
||||||
"html": "0.0.10",
|
"html": "0.0.10",
|
||||||
"is-my-json-valid": "^2.12.2",
|
"is-my-json-valid": "^2.12.2",
|
||||||
|
168
src/fluentcmd.js
Normal file
168
src/fluentcmd.js
Normal 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
|
||||||
|
};
|
||||||
|
|
||||||
|
}();
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}());
|
|
@ -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;
|
|
@ -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;
|
|
@ -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' );
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
@ -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' )
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}());
|
|
@ -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;
|
|
@ -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' );
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
@ -1,22 +1,22 @@
|
|||||||
#! /usr/bin/env node
|
#! /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.
|
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var ARGS = require( 'minimist' )
|
var ARGS = require( 'minimist' )
|
||||||
, HMR = require( './scrappy');
|
, FCMD = require( './fluentcmd');
|
||||||
|
|
||||||
try {
|
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.'; }
|
if( process.argv.length <= 2 ) { throw 'Please specify a JSON resume file.'; }
|
||||||
|
|
||||||
var args = ARGS( process.argv.slice(2) );
|
var args = ARGS( process.argv.slice(2) );
|
||||||
var src = args._.filter( function( a ) { return a.endsWith('.json'); });
|
var src = args._.filter( function( a ) { return a.endsWith('.json'); });
|
||||||
var dst = 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 ) {
|
catch( ex ) {
|
||||||
|
@ -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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
296
src/scrappy.js
296
src/scrappy.js
@ -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
|
|
||||||
};
|
|
||||||
|
|
||||||
}();
|
|
185
src/sheet.js
185
src/sheet.js
@ -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;
|
|
||||||
|
|
||||||
}());
|
|
Loading…
Reference in New Issue
Block a user