diff --git a/README.md b/README.md index 05b5561..41c6a1c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index ee62142..24d72c9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/fluentcmd.js b/src/fluentcmd.js new file mode 100644 index 0000000..c5af92f --- /dev/null +++ b/src/fluentcmd.js @@ -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\>\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 + }; + +}(); diff --git a/src/gen/base-generator.js b/src/gen/base-generator.js deleted file mode 100644 index e53d908..0000000 --- a/src/gen/base-generator.js +++ /dev/null @@ -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; - } - }); -}()); diff --git a/src/gen/html-generator.js b/src/gen/html-generator.js deleted file mode 100644 index 0531913..0000000 --- a/src/gen/html-generator.js +++ /dev/null @@ -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; diff --git a/src/gen/html-pdf-generator.js b/src/gen/html-pdf-generator.js deleted file mode 100644 index a657204..0000000 --- a/src/gen/html-pdf-generator.js +++ /dev/null @@ -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; diff --git a/src/gen/markdown-generator.js b/src/gen/markdown-generator.js deleted file mode 100644 index 330e89b..0000000 --- a/src/gen/markdown-generator.js +++ /dev/null @@ -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' ); - } - -}); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js deleted file mode 100644 index a1e08a0..0000000 --- a/src/gen/template-generator.js +++ /dev/null @@ -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\>\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' ) - }; - - - -}()); diff --git a/src/gen/text-generator.js b/src/gen/text-generator.js deleted file mode 100644 index 5aa077b..0000000 --- a/src/gen/text-generator.js +++ /dev/null @@ -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; diff --git a/src/gen/word-generator.js b/src/gen/word-generator.js deleted file mode 100644 index db6cbd8..0000000 --- a/src/gen/word-generator.js +++ /dev/null @@ -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' ); - }, - -}); diff --git a/src/index.js b/src/index.js index 813d919..f6f0467 100644 --- a/src/index.js +++ b/src/index.js @@ -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 ) { diff --git a/src/resume-schema.json b/src/resume-schema.json deleted file mode 100644 index 44b75c8..0000000 --- a/src/resume-schema.json +++ /dev/null @@ -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." - } - } - - } - } - } -} diff --git a/src/scrappy.js b/src/scrappy.js deleted file mode 100644 index a67eee6..0000000 --- a/src/scrappy.js +++ /dev/null @@ -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\>\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 - }; - -}(); diff --git a/src/sheet.js b/src/sheet.js deleted file mode 100644 index c488506..0000000 --- a/src/sheet.js +++ /dev/null @@ -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; - -}());