mirror of
https://github.com/JuanCanham/HackMyResume.git
synced 2024-11-05 09:56:22 +00:00
Introduce scrappy sources from main dev tree.
Add existing "HackMyResume" command-line sources from commit 59 on the original dev tree, without modification except for a partial rename of "HackMyResume" to "scrappy". See: https://github.com/gruebait/HackMyResume/tree/master/HMR.Console
This commit is contained in:
parent
9216b9cafb
commit
3b92065c14
36
package.json
Normal file
36
package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "scrappy",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "An extensible command-line-based resume generator for the 21st century.",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/gruebait/scrappy.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"resume",
|
||||||
|
"CV",
|
||||||
|
"portfolio",
|
||||||
|
"Markdown"
|
||||||
|
],
|
||||||
|
"author": "James M. Devlin",
|
||||||
|
"license": "Proprietary and confidential. Copyright (c) 2015 by James M. Devlin. All rights reserved.",
|
||||||
|
"preferGlobal": "true",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/gruebait/scrappy/issues"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"scrappy": "src/index.js"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/gruebait/scrappy",
|
||||||
|
"dependencies": {
|
||||||
|
"fs-extra": "^0.24.0",
|
||||||
|
"html": "0.0.10",
|
||||||
|
"jst": "0.0.13",
|
||||||
|
"marked": "^0.3.5",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"phantom": "^0.7.2",
|
||||||
|
"underscore": "^1.8.3",
|
||||||
|
"wkhtmltopdf": "^0.1.5",
|
||||||
|
"xml-escape": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
78
src/extend.js
Normal file
78
src/extend.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
Plain JavaScript replacement of jQuery .extend based on jQuery sources.
|
||||||
|
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function _extend() {
|
||||||
|
|
||||||
|
function isPlainObject( obj ) {
|
||||||
|
if ((typeof obj !== "object") || obj.nodeType ||
|
||||||
|
(obj !== null && obj === obj.window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (obj.constructor &&
|
||||||
|
!hasOwnProperty.call( obj.constructor.prototype, "isPrototypeOf" )) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options
|
||||||
|
, name
|
||||||
|
, src
|
||||||
|
, copy
|
||||||
|
, copyIsArray
|
||||||
|
, clone
|
||||||
|
, target = arguments[0] || {}
|
||||||
|
, i = 1
|
||||||
|
, length = arguments.length
|
||||||
|
, deep = false;
|
||||||
|
|
||||||
|
// Handle a deep copy situation
|
||||||
|
if (typeof target === "boolean") {
|
||||||
|
deep = target;
|
||||||
|
// Skip the boolean and the target
|
||||||
|
target = arguments[i] || {};
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle case when target is a string or something (possible in deep copy)
|
||||||
|
//if (typeof target !== "object" && !jQuery.isFunction(target))
|
||||||
|
if (typeof target !== "object" && typeof target !== "function")
|
||||||
|
target = {};
|
||||||
|
|
||||||
|
for (; i < length; i++) {
|
||||||
|
// Only deal with non-null/undefined values
|
||||||
|
if ((options = arguments[i]) !== null) {
|
||||||
|
// Extend the base object
|
||||||
|
for (name in options) {
|
||||||
|
src = target[name];
|
||||||
|
copy = options[name];
|
||||||
|
|
||||||
|
// Prevent never-ending loop
|
||||||
|
if (target === copy) continue;
|
||||||
|
|
||||||
|
// Recurse if we're merging plain objects or arrays
|
||||||
|
if (deep && copy && (isPlainObject(copy) ||
|
||||||
|
(copyIsArray = (copy.constructor === Array)))) {
|
||||||
|
if (copyIsArray) {
|
||||||
|
copyIsArray = false;
|
||||||
|
clone = src && (src.constructor === Array) ? src : [];
|
||||||
|
} else {
|
||||||
|
clone = src && isPlainObject(src) ? src : {};
|
||||||
|
}
|
||||||
|
// Never move original objects, clone them
|
||||||
|
target[name] = _extend(deep, clone, copy);
|
||||||
|
// Don't bring in undefined values
|
||||||
|
} else if (copy !== undefined) {
|
||||||
|
target[name] = copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the modified object
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = _extend;
|
29
src/index.js
Normal file
29
src/index.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#! /usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
Command-line resume generation logic for Scrappy.
|
||||||
|
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var ARGS = require( 'minimist' )
|
||||||
|
, HMR = require( './hmr');
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
console.log( '*** Scrappy 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' );
|
||||||
|
|
||||||
|
}
|
||||||
|
catch( ex ) {
|
||||||
|
|
||||||
|
var msg = ex.toString();
|
||||||
|
var idx = msg.indexOf('Error: ');
|
||||||
|
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
|
||||||
|
console.log( 'ERROR: ' + trimmed.toString() );
|
||||||
|
|
||||||
|
}
|
272
src/scrappy.js
Normal file
272
src/scrappy.js
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
/**
|
||||||
|
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( './extend' )
|
||||||
|
, _ = require('underscore');
|
||||||
|
|
||||||
|
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 );
|
||||||
|
var raw = FS.readFileSync( res, 'utf8' );
|
||||||
|
return JSON.parse( raw );
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge input resumes
|
||||||
|
rez = sheets.reduce( function( acc, elem ) {
|
||||||
|
return extend(true, acc, elem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the transformation!
|
||||||
|
targets.map( gen );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Generate a single resume of a specific format.
|
||||||
|
*/
|
||||||
|
function gen( f ) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Load the theme template
|
||||||
|
var fName = f.substring( f.lastIndexOf('.') + 1 );
|
||||||
|
var fObj = _fmts.filter( function(_f) { return _f.name === fName; } )[0];
|
||||||
|
var fOut = path.join( process.cwd(),
|
||||||
|
f.substring( 0, f.lastIndexOf('.') + 1 ) + fObj.ext );
|
||||||
|
console.log( 'Generating ' + fName.toUpperCase() + ' resume: ' + fOut );
|
||||||
|
var themeFile = path.join( __dirname, '/../themes/', _opts.theme,
|
||||||
|
fName + '.' + (fObj.fmt || fObj.ext));
|
||||||
|
var cssData = (fName != 'html' && fName != 'pdf') ? null :
|
||||||
|
FS.readFileSync( path.join( __dirname, '/../themes/', _opts.theme, 'html.css' ), 'utf8' );
|
||||||
|
var mk = FS.readFileSync( themeFile, 'utf8' );
|
||||||
|
|
||||||
|
// Compile and invoke the template
|
||||||
|
mk = single( rez, mk, fName, cssData );
|
||||||
|
|
||||||
|
// Post-process and save the file
|
||||||
|
fName === 'html' && (mk = html( mk, themeFile, fOut ));
|
||||||
|
fName === 'pdf' && pdf( mk, fOut );
|
||||||
|
fName !== 'pdf' && FS.writeFileSync( fOut, mk, 'utf8' );
|
||||||
|
}
|
||||||
|
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 ) {
|
||||||
|
|
||||||
|
// 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, 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: 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
}();
|
Loading…
Reference in New Issue
Block a user