diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b6c6453 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +Contributing +============ + +*Note: HackMyResume is also available as [FluentCV][fcv]. Contributors are +credited in both.* + +HackMyResume needs your help! Our contribution workflow is based on [GitHub +Flow][flow] and we respond to all pull requests and issues, usually within 24 +hours. HackMyResume has no corporate affiliation and no commercial basis, which +allows the project to maintain a strict user-first policy, rapid development +velocity, and a liberal stance on contributions and exotic functionality in +keeping with the spirit (and name) of the tool. + +In short, your code is welcome here. + +## How To Contribute + +1. Optional: [**open an issue**][iss] identifying the feature or bug you'd like +to implement or fix. This step isn't required — you can start hacking away on +HackMyResume without clearing it with us — but helps avoid duplication of work +and ensures that your changes will be accepted once submitted. +2. **Fork and clone** the HackMyResume project. +3. Ideally, **create a new feature branch** (eg, `feat/new-awesome-feature` or +similar; call it whatever you like) to perform your work in. +4. **Install dependencies** by running `npm install` in the top-level +HackMyResume folder. +5. Make your **commits** as usual. +6. **Verify** your changes locally with `npm test`. +7. **Push** your commits. +7. **Submit a pull request** from your feature branch to the HackMyResume `dev` +branch. +8. We'll typically **respond** within 24 hours. +9. Your awesome changes will be **merged** after verification. + +## Project Maintainers + +HackMyResume is currently maintained by [hacksalot][ha] with assistance from +[tomheon][th] and our awesome [contributors][awesome]. Please direct all official +or internal inquiries to: + +``` +admin@hackmyresume.com +``` + +You can reach hacksalot directly at: + +``` +hacksalot@indevious.com +``` + +Thanks! See you out there in the trenches. + +[fcv]: https://github.com/fluentdesk/fluentcv +[flow]: https://guides.github.com/introduction/flow/ +[iss]: https://github.com/hacksalot/HackMyResume/issues +[ha]: https://github.com/hacksalot +[th]: https://github.com/tomheon +[awesome]: https://github.com/hacksalot/HackMyResume/graphs/contributors diff --git a/Gruntfile.js b/Gruntfile.js index 45cd6ca..536fe8d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -14,10 +14,10 @@ module.exports = function (grunt) { ui: 'bdd', reporter: 'spec' }, - all: { src: ['tests/*.js'] } + all: { src: ['test/*.js'] } }, - clean: ['tests/sandbox'], + clean: ['test/sandbox'], yuidoc: { compile: { @@ -38,7 +38,7 @@ module.exports = function (grunt) { laxcomma: true, expr: true }, - all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'] + all: ['Gruntfile.js', 'src/**/*.js', 'test/*.js'] } }; @@ -51,9 +51,9 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-contrib-clean'); grunt.registerTask('test', 'Test the HackMyResume library.', - function( config ) { grunt.task.run( ['clean','simplemocha:all'] ); }); + function( config ) { grunt.task.run( ['clean','jshint','simplemocha:all'] ); }); grunt.registerTask('document', 'Generate HackMyResume library documentation.', function( config ) { grunt.task.run( ['yuidoc'] ); }); - grunt.registerTask('default', [ 'jshint', 'test', 'yuidoc' ]); + grunt.registerTask('default', [ 'test', 'yuidoc' ]); }; diff --git a/README.md b/README.md index 141d7d8..e58b005 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ or Windows. - Support for multiple input and output resumes. - Use from your command line or [desktop][7]. - Free and open-source through the MIT license. +- Updated daily. ## Install @@ -42,8 +43,36 @@ Install HackMyResume with NPM: ``` Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for -your platform. For LaTeX generation you'll need a valid LaTeX environment with -access to `xelatex` and similar. +your platform. + +## Installing Themes + +HackMyResume supports both [FRESH][fresh-themes] and [JSON Resume][jrst]-style +résumé themes. + +- FRESH themes currently come preinstalled with HackMyResume. +- JSON Resume themes can be installed from NPM, GitHub, or manually. + +To install a JSON Resume theme, just `cd` to the folder where you want to store +your themes and run one of: + +```bash +# Install with NPM +npm install jsonresume-theme-[theme-name] + +# Install with GitHub +git clone https://github.com/[user-or-org]/[repo-name] +``` + +Then when you're ready to generate your resume, just reference the location of +the theme folder as you installed it: + +```bash +hackmyresume BUILD resume.json TO out/resume.all -t node_modules/jsonresume-theme-classy +``` + +Note: You can use install themes anywhere on your file system. You don't need a +package.json or other NPM/Node infrastructure. ## Getting Started @@ -98,9 +127,9 @@ Output Format | Ext | Notes ------------- | --- | ----- HTML | .html | A standard HTML 5 + CSS resume format that can be viewed in a browser, deployed to a website, etc. Markdown | .md | A structured Markdown document that can be used as-is or used to generate HTML. -LaTeX | .tex | A structured LaTeX document (or collection of documents). -MS Word | .doc | A Microsoft Word office document. -Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme. +LaTeX | .tex | A structured LaTeX document (or collection of documents) that can be processed with pdflatex, xelatex, and similar tools. +MS Word | .doc | A Microsoft Word office document (XML-driven; WordProcessingML). +Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme (through wkhtmltopdf). plain text | .txt | A formatted plain text document appropriate for emails or copy-paste. JSON | .json | A JSON representation of the resume. YAML | .yml | A YAML representation of the resume. @@ -108,15 +137,6 @@ RTF | .rtf | Forthcoming. Textile | .textile | Forthcoming. image | .png, .bmp | Forthcoming. -## Install - -HackMyResume requires a recent version of [Node.js][4] and [NPM][5]. Then: - -1. Install the latest official [wkhtmltopdf][3] binary for your platform. -2. Optionally install an updated LaTeX environment (LaTeX resumes only). -2. Install **HackMyResume** with `[sudo] npm install hackmyresume -g`. -3. You're ready to go. - ## Use Assuming you've got a JSON-formatted resume handy, generating resumes in @@ -132,19 +152,19 @@ theme (default to Modern). For example: ```bash # Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.) -hackmyresume build resume.json -o out/resume.all -t modern +hackmyresume BUILD resume.json TO out/resume.all -t modern # Generate a specific resume format -hackmyresume build resume.json TO out/resume.html -hackmyresume build resume.json TO out/resume.pdf -hackmyresume build resume.json TO out/resume.md -hackmyresume build resume.json TO out/resume.doc -hackmyresume build resume.json TO out/resume.json -hackmyresume build resume.json TO out/resume.txt -hackmyresume build resume.json TO out/resume.yml +hackmyresume BUILD resume.json TO out/resume.html +hackmyresume BUILD resume.json TO out/resume.pdf +hackmyresume BUILD resume.json TO out/resume.md +hackmyresume BUILD resume.json TO out/resume.doc +hackmyresume BUILD resume.json TO out/resume.json +hackmyresume BUILD resume.json TO out/resume.txt +hackmyresume BUILD resume.json TO out/resume.yml # Specify 2 inputs and 3 outputs -hackmyresume build in1.json in2.json TO out.html out.doc out.pdf +hackmyresume BUILD in1.json in2.json TO out.html out.doc out.pdf ``` You should see something to the effect of: @@ -171,8 +191,8 @@ For a predefined theme, include the theme name. For a custom theme, include the path to the custom theme's folder. ```bash -hackmyresume build resume.json -t modern -hackmyresume build resume.json -t ~/foo/bar/my-custom-theme/ +hackmyresume BUILD resume.json TO out/rez.all -t modern +hackmyresume BUILD resume.json TO OUT.rez.all -t ~/foo/bar/my-custom-theme/ ``` As of v1.0.0, available predefined themes are `positive`, `modern`, `compact`, @@ -185,7 +205,7 @@ most generic to most specific: ```bash # Merge specific.json onto base.json and generate all formats -hackmyresume build base.json specific.json -o resume.all +hackmyresume BUILD base.json specific.json TO resume.all ``` This can be useful for overriding a base (generic) resume with information from @@ -196,7 +216,7 @@ resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge: ```bash -hackmyresume build in1.json in2.json in3.json in4.json TO out.html out.doc +hackmyresume BUILD in1.json in2.json in3.json in4.json TO out.html out.doc Reading JSON resume: in1.json Reading JSON resume: in2.json Reading JSON resume: in3.json @@ -212,14 +232,7 @@ You can specify **multiple output targets** and HackMyResume will build them: ```bash # Generate out1.doc, out1.pdf, and foo.txt from me.json. -hackmyresume build me.json -o out1.doc -o out1.pdf -o foo.txt -``` - -You can also omit the output file(s) and/or theme completely: - -```bash -# Equivalent to "hackmyresume resume.json resume.all -t modern" -hackmyresume build resume.json +hackmyresume BUILD me.json TO out1.doc out1.pdf foo.txt ``` ### Using .all @@ -229,7 +242,7 @@ formats for the given resume. For example, this... ```bash # Generate all resume formats (HTML, PDF, DOC, TXT, etc.) -hackmyresume build me.json -o out/resume.all +hackmyresume BUILD me.json TO out/resume.all ``` ..tells HackMyResume to read `me.json` and generate `out/resume.md`, @@ -244,7 +257,7 @@ resumes, use the `validate` command: ```bash # Validate myresume.json against either the FRESH or JSON Resume schema. -hackmyresume validate resumeA.json resumeB.json +hackmyresume VALIDATE resumeA.json resumeB.json ``` HackMyResume will validate each specified resume in turn: @@ -276,7 +289,7 @@ HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag can be used: ```bash -hackmyresume generate resume.json out.all --nopretty +hackmyresume BUILD resume.json out.all --nopretty ``` ### Silent Mode @@ -284,10 +297,16 @@ hackmyresume generate resume.json out.all --nopretty Use `-s` or `--silent` to run in silent mode: ```bash -hackmyresume generate resume.json -o someFile.all -s -hackmyresume generate resume.json -o someFile.all --silent +hackmyresume BUILD resume.json -o someFile.all -s +hackmyresume BUILD resume.json -o someFile.all --silent ``` +## Contributing + +HackMyResume is a community-driven free and open source project under the MIT +License. Contributions are encouraged and we respond to all PRs and issues, +usually within 24 hours. See [CONTRIBUTING.md][contribute] for details. + ## License MIT. Go crazy. See [LICENSE.md][1] for details. @@ -307,3 +326,6 @@ MIT. Go crazy. See [LICENSE.md][1] for details. [dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself [travis-image]: https://img.shields.io/travis/palomajs/paloma.svg?style=flat-square [travis-url]: https://travis-ci.org/hacksalot/HackMyResume +[contribute]: CONTRIBUTING.md +[fresh-themes]: https://github.com/fluentdesk/fluent-themes +[jrst]: https://www.npmjs.com/search?q=jsonresume-theme diff --git a/package.json b/package.json index ea6ef34..8eb6ba2 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "hackmyresume", - "version": "1.2.2", + "version": "1.3.0-beta", "description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.", "repository": { "type": "git", "url": "https://github.com/hacksalot/HackMyResume.git" }, "scripts": { - "test": "mocha && grunt", + "test": "grunt clean && mocha", "grunt": "grunt" }, "keywords": [ @@ -29,6 +29,11 @@ "template" ], "author": "hacksalot (https://github.com/hacksalot)", + "contributors": [ + "Edmund Jorgensen (https://github.com/tomheon)", + "Ya Zhuang (https://github.com/zhuangya)", + "hacksalot (https://github.com/hacksalot)" + ], "license": "MIT", "preferGlobal": "true", "bugs": { @@ -41,7 +46,8 @@ "homepage": "https://github.com/hacksalot/HackMyResume", "dependencies": { "colors": "^1.1.2", - "fluent-themes": "~0.7.1-beta", + "copy": "^0.1.3", + "fluent-themes": "~0.8.0-beta", "fresca": "~0.2.2", "fs-extra": "^0.24.0", "handlebars": "^4.0.5", @@ -57,6 +63,7 @@ "path-exists": "^2.1.0", "recursive-readdir-sync": "^1.0.6", "simple-html-tokenizer": "^0.2.0", + "string.prototype.startswith": "^0.2.0", "underscore": "^1.8.3", "webshot": "^0.16.0", "wkhtmltopdf": "^0.1.5", @@ -72,6 +79,10 @@ "grunt-contrib-yuidoc": "^0.10.0", "grunt-simple-mocha": "*", "jane-q-fullstacker": "fluentdesk/jane-q-fullstacker", + "jsonresume-theme-boilerplate": "^0.1.2", + "jsonresume-theme-classy": "^1.0.9", + "jsonresume-theme-modern": "0.0.18", + "jsonresume-theme-sceptile": "^1.0.5", "mocha": "*", "resample": "fluentdesk/resample" } diff --git a/src/core/error-handler.js b/src/core/error-handler.js new file mode 100644 index 0000000..53aa014 --- /dev/null +++ b/src/core/error-handler.js @@ -0,0 +1,93 @@ +/** +@module error-handler.js +*/ + +(function() { + + var HACKMYSTATUS = require('./status-codes') + , PKG = require('../../package.json') + , title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white; + + var ErrorHandler = module.exports = { + + + err: function( ex, shouldExit ) { + var msg = '', exitCode; + + if( ex.fluenterror ){ + switch( ex.fluenterror ) { // TODO: Remove magic numbers + + case HACKMYSTATUS.themeNotFound: + msg = "The specified theme couldn't be found: " + ex.data; + break; + + case HACKMYSTATUS.copyCSS: + msg = "Couldn't copy CSS file to destination folder"; + break; + + case HACKMYSTATUS.resumeNotFound: + msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + + ' in FRESH or JSON Resume format.'.guide; + break; + + case HACKMYSTATUS.missingCommand: + msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide + + Object.keys( FCMD.verbs ).map( function(v, idx, ar) { + return (idx === ar.length - 1 ? 'or '.guide : '') + + v.toUpperCase().guide; + }).join(', '.guide) + ").\n\n".guide + + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; + break; + + case HACKMYSTATUS.invalidCommand: + msg = 'Please '.guide + 'specify the output resume file'.guide.bold + + ' that should be created.'.guide; + break; + + case HACKMYSTATUS.resumeNotFoundAlt: + msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + + ' in either FRESH or JSON Resume format.'.guide; + break; + + case HACKMYSTATUS.inputOutputParity: + msg = 'Please '.guide + 'specify an output file name'.guide.bold + + ' for every input file you wish to convert.'.guide; + break; + + case HACKMYSTATUS.createNameMissing: + msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold + + ' to create.'.guide; + break; + + case HACKMYSTATUS.wkhtmltopdf: + msg = 'ERROR: PDF generation failed. '.red.bold + ('Make sure wkhtmltopdf is ' + + 'installed and accessible from your path.').red; + break; + + } + exitCode = ex.fluenterror; + + } + else { + msg = ex.toString(); + exitCode = 4; + } + + var idx = msg.indexOf('Error: '); + var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); + if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s + console.log( ('ERROR: ' + trimmed.toString()).red.bold ); + console.log( ex.stack.gray); + } + else { + console.log( trimmed.toString() ); + } + + if( shouldExit ) + process.exit( exitCode ); + + } + + }; + +}()); diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index 4754194..58c97a5 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -143,15 +143,12 @@ Definition of the FRESHResume class. }; /** - Open and parse the specified JSON resume sheet. Merge the JSON object model - onto this Sheet instance with extend() and convert sheet dates to a safe & + Initialize the FreshResume from JSON data. + Open and parse the specified FRESH resume. Merge the JSON object model onto + this Sheet instance with extend() and convert sheet dates to a safe & consistent format. Then sort each section by startDate descending. */ - FreshResume.prototype.parse = function( stringData, opts ) { - - // Parse the incoming JSON representation - var rep = JSON.parse( stringData ); - + FreshResume.prototype.parseJSON = function( rep, opts ) { // Convert JSON Resume to FRESH if necessary if( rep.basics ) { rep = CONVERTER.toFRESH( rep ); @@ -178,6 +175,13 @@ Definition of the FRESHResume class. return this; }; + /** + Initialize the the FreshResume from string data. + */ + FreshResume.prototype.parse = function( stringData, opts ) { + return this.parseJSON( JSON.parse( stringData ), opts ); + }; + /** Return a unique list of all keywords across all skills. */ diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js index c9311a0..49a8d86 100644 --- a/src/core/jrs-resume.js +++ b/src/core/jrs-resume.js @@ -11,6 +11,7 @@ Definition of the JRSResume class. , validator = require('is-my-json-valid') , _ = require('underscore') , PATH = require('path') + , MD = require('marked') , moment = require('moment'); /** @@ -70,6 +71,7 @@ Definition of the JRSResume class. }; /** + Initialize the JRS Resume from string data. Open and parse the specified JSON resume sheet. Merge the JSON object model onto this Sheet instance with extend() and convert sheet dates to a safe & consistent format. Then sort each section by startDate descending. @@ -77,7 +79,14 @@ Definition of the JRSResume class. JRSResume.prototype.parse = function( stringData, opts ) { opts = opts || { }; var rep = JSON.parse( stringData ); + return this.parseJSON( rep, opts ); + }; + /** + Initialize the JRSRume from JSON data. + */ + JRSResume.prototype.parseJSON = function( rep, opts ) { + opts = opts || { }; extend( true, this, rep ); // Set up metadata if( opts.imp === undefined || opts.imp ) { @@ -230,6 +239,70 @@ Definition of the JRSResume class. }; + JRSResume.prototype.dupe = function() { + var rnew = new JRSResume(); + rnew.parse( this.stringify(), { } ); + return rnew; + }; + + /** + Create a copy of this resume in which all fields have been interpreted as + Markdown. + */ + JRSResume.prototype.harden = function() { + + var that = this; + var ret = this.dupe(); + + function HD(txt) { + return '@@@@~' + txt + '~@@@@'; + } + + function HDIN(txt){ + //return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); + return HD(txt); + } + + // TODO: refactor recursion + function hardenStringsInObject( obj, inline ) { + + if( !obj ) return; + inline = inline === undefined || inline; + + + if( Object.prototype.toString.call( obj ) === '[object Array]' ) { + obj.forEach(function(elem, idx, ar){ + if( typeof elem === 'string' || elem instanceof String ) + ar[idx] = inline ? HDIN(elem) : HD( elem ); + else + hardenStringsInObject( elem ); + }); + } + else if (typeof obj === 'object') { + Object.keys( obj ).forEach(function(key) { + var sub = obj[key]; + if( typeof sub === 'string' || sub instanceof String ) { + if( _.contains(['skills','url','website','startDate','endDate','releaseDate','date','phone','email','address','postalCode','city','country','region'], key) ) + return; + if( key === 'summary' ) + obj[key] = HD( obj[key] ); + else + obj[key] = inline ? HDIN( obj[key] ) : HD( obj[key] ); + } + else + hardenStringsInObject( sub ); + }); + } + + } + + Object.keys( ret ).forEach(function(member){ + hardenStringsInObject( ret[ member ] ); + }); + + return ret; + }; + /** 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 diff --git a/src/core/load-source-resumes.js b/src/core/load-source-resumes.js deleted file mode 100644 index b26ae70..0000000 --- a/src/core/load-source-resumes.js +++ /dev/null @@ -1,13 +0,0 @@ -(function(){ - - var FRESHResume = require('../core/fresh-resume'); - - module.exports = function loadSourceResumes( src, log, fn ) { - return src.map( function( res ) { - log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info + - res.cyan.bold ); - return (fn && fn(res)) || (new FRESHResume()).open( res ); - }); - }; - -}()); diff --git a/src/core/resume-factory.js b/src/core/resume-factory.js new file mode 100644 index 0000000..b72f8a9 --- /dev/null +++ b/src/core/resume-factory.js @@ -0,0 +1,43 @@ +/** +Core resume-loading logic for HackMyResume. +@module resume-factory.js +*/ + +(function(){ + + require('string.prototype.startswith'); + var FS = require('fs'); + var ResumeConverter = require('./convert'); + + /** + A simple factory class for FRESH and JSON Resumes. + @class ResumeFactory + */ + module.exports = { + + /** + Load one or more resumes in a specific source format. + */ + load: function ( src, log, fn, toFormat ) { + + toFormat = toFormat && (toFormat.toLowerCase().trim()) || 'fresh'; + var ResumeClass = require('../core/' + toFormat + '-resume'); + + return src.map( function( res ) { + var rezJson = JSON.parse( FS.readFileSync( res ) ); + var orgFormat = ( rezJson.meta && rezJson.meta.format && + rezJson.meta.format.startsWith('FRESH@') ) ? + 'fresh' : 'jrs'; + if(orgFormat !== toFormat) { + rezJson = ResumeConverter[ 'to' + toFormat.toUpperCase() ]( rezJson ); + } + // TODO: Core should not log + log( 'Reading '.info + orgFormat.toUpperCase().infoBold + ' resume: '.info + res.cyan.bold ); + return (fn && fn(res)) || (new ResumeClass()).parseJSON( rezJson ); + }); + + } + + }; + +}()); diff --git a/src/core/spawn-watch.js b/src/core/spawn-watch.js new file mode 100644 index 0000000..ac06849 --- /dev/null +++ b/src/core/spawn-watch.js @@ -0,0 +1,22 @@ +/** +@module spawn-watch.js +*/ + +(function() { + +// Catch various out-of-band child process errors such as ENOENT for PDFs +// http://stackoverflow.com/q/27688804 +var SpawnWatcher = module.exports = function() { + var childProcess = require("child_process"); + var oldSpawn = childProcess.spawn; + childProcess.spawn = function() { + return oldSpawn.apply(this, arguments) + .on('error', function(err) { + require('./error-handler').err( err, false ); + }); + }; +}(); + +//SpawnWatcher(); + +}()); diff --git a/src/core/status-codes.js b/src/core/status-codes.js new file mode 100644 index 0000000..d0467c7 --- /dev/null +++ b/src/core/status-codes.js @@ -0,0 +1,21 @@ +/** +Status codes for HackMyResume. +@module status-codes.js +*/ + +(function(){ + + module.exports = { + success: 0, + themeNotFound: 1, + copyCss: 2, + resumeNotFound: 3, + missingCommand: 4, + invalidCommand: 5, + resumeNotFoundAlt: 6, + inputOutputParity: 7, + createNameMissing: 8, + wkhtmltopdf: 9 + }; + +}()); diff --git a/src/core/theme.js b/src/core/theme.js index d2ef909..b9fe152 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -1,6 +1,6 @@ /** Definition of the Theme class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@license MIT. Copyright (c) 2015 hacksalot / FluentDesk. @module theme.js */ @@ -12,6 +12,7 @@ Definition of the Theme class. , _ = require('underscore') , PATH = require('path') , parsePath = require('parse-filepath') + , pathExists = require('path-exists').sync , EXTEND = require('../utils/extend') , moment = require('moment') , RECURSIVE_READ_DIR = require('recursive-readdir-sync'); @@ -29,9 +30,26 @@ Definition of the Theme class. */ Theme.prototype.open = function( themeFolder ) { - // Open the [theme-name].json file; should have the same name as folder this.folder = themeFolder; + + // Open the [theme-name].json file; should have the same name as folder var pathInfo = parsePath( themeFolder ); + + // Set up a formats hash for the theme + var formatsHash = { }; + + // See if the theme has a package.json. If so, load it. + var packageJsonPath = PATH.join(themeFolder, 'package.json'); + if( pathExists( packageJsonPath ) ) { + var themePack = require( themeFolder ); + var themePkgJson = require( packageJsonPath ); + this.name = themePkgJson.name; + this.render = (themePack && themePack.render) || undefined; + this.formats = { html: { title: 'html', outFormat: 'html', ext: 'html', path: null, data: null } }; + return this; + } + + // Otherwise, do a full theme load var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' ); var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) ); var that = this; @@ -39,9 +57,6 @@ Definition of the Theme class. // Move properties from the theme JSON file to the theme object EXTEND( true, this, themeInfo ); - // Set up a formats has for the theme - var formatsHash = { }; - // Check for an explicit "formats" entry in the theme JSON. If it has one, // then this theme declares its files explicitly. if( !!this.formats ) { diff --git a/src/gen/base-generator.js b/src/gen/base-generator.js index 9e1bc89..5d11777 100644 --- a/src/gen/base-generator.js +++ b/src/gen/base-generator.js @@ -26,14 +26,7 @@ Definition of the BaseGenerator class. /** Status codes. */ - codes: { - success: 0, - themeNotFound: 1, - copyCss: 2, - resumeNotFound: 3, - missingCommand: 4, - invalidCommand: 5 - }, + codes: require('../core/status-codes'), /** Generator options. diff --git a/src/gen/html-pdf-generator.js b/src/gen/html-pdf-generator.js index 853de90..705c4b3 100644 --- a/src/gen/html-pdf-generator.js +++ b/src/gen/html-pdf-generator.js @@ -1,6 +1,5 @@ /** Definition of the HtmlPdfGenerator class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module html-pdf-generator.js */ @@ -23,7 +22,7 @@ Definition of the HtmlPdfGenerator class. Generate the binary PDF. */ onBeforeSave: function( info ) { - pdf( info.mk, info.outputFile ); + pdf.call( this, info.mk, info.outputFile ); return null; // halt further processing } @@ -34,39 +33,50 @@ Definition of the HtmlPdfGenerator class. */ function pdf( markup, fOut ) { - var pdfCount = 0; - if( false ) { //( _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 } } ); - }); + pdf_wkhtmltopdf.call( this, markup, fOut ); + + } + + /** + Generate a PDF from HTML using wkhtmltopdf. + */ + function pdf_wkhtmltopdf( markup, fOut ) { + var wk; + try { + wk = require('wkhtmltopdf'); + wk( markup, { pageSize: 'letter' } ) + .pipe( FS.createWriteStream( fOut ) ); } - if( true ) { // _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++; + catch(ex) { + // { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', syscall: 'write' } + // { [Error: ENOENT] } + throw { fluenterror: this.codes.wkhtmltopdf }; } } + + // function pdf_phantom() { + // 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 } } ); + // }); + // } + }()); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 86687e8..60a7a6f 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -156,7 +156,7 @@ Definition of the TemplateGenerator class. { outputFile: fileName, mk: file.data } ); } catch(ex) { - console.log(ex); + require('../core/error-handler').err(ex, false); } } else if( file.info.action === null/* && theme.explicit*/ ) { diff --git a/src/index.js b/src/index.js index 5b65b71..48d2503 100644 --- a/src/index.js +++ b/src/index.js @@ -2,27 +2,32 @@ /** Command-line interface (CLI) for HackMyResume. -@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk. +@license MIT. Copyright (c) 2015 hacksalot (https://github.com/hacksalot) @module index.js */ -var ARGS = require( 'minimist' ) + + +var SPAWNW = require('./core/spawn-watch') + , ARGS = require( 'minimist' ) , FCMD = require( './hackmycmd') , PKG = require('../package.json') , COLORS = require('colors') , FS = require('fs') , PATH = require('path') + , HACKMYSTATUS = require('./core/status-codes') , opts = { } , title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white , _ = require('underscore'); + try { main(); } catch( ex ) { - handleError( ex ); + require('./core/error-handler').err( ex, true ); } @@ -90,47 +95,3 @@ function getOpts( args ) { silent: args.s || args.silent }; } - -// TODO: refactor -function handleError( ex ) { - var msg = '', exitCode; - - - - if( ex.fluenterror ){ - switch( ex.fluenterror ) { // TODO: Remove magic numbers - case 1: msg = "The specified theme couldn't be found: " + ex.data; break; - case 2: msg = "Couldn't copy CSS file to destination folder"; break; - case 3: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in FRESH or JSON Resume format.'.guide; break; - case 4: msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide + - Object.keys( FCMD.verbs ).map( function(v, idx, ar) { - return (idx === ar.length - 1 ? 'or '.guide : '') + - v.toUpperCase().guide; - }).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; - break; - //case 4: msg = title + '\n' + ; break; - case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created.'.guide; break; - case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break; - case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break; - case 8: msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold + ' to create.'.guide; break; - } - exitCode = ex.fluenterror; - - } - else { - msg = ex.toString(); - exitCode = 4; - } - - var idx = msg.indexOf('Error: '); - var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); - if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s - console.log( ('ERROR: ' + trimmed.toString()).red.bold ); - console.log( ex.stack.gray); - } - else - console.log( trimmed.toString() ); - - process.exit( exitCode ); - -} diff --git a/src/utils/string.js b/src/utils/string.js index 9da084f..c5b759f 100644 --- a/src/utils/string.js +++ b/src/utils/string.js @@ -1,6 +1,5 @@ /** Definitions of string utility functions. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module string.js */ @@ -10,14 +9,18 @@ See: http://stackoverflow.com/a/32800728/4942583 @method isNullOrWhitespace */ -String.isNullOrWhitespace = function( input ) { - return !input || !input.trim(); -}; +(function() { -String.prototype.endsWith = function(suffix) { - return this.indexOf(suffix, this.length - suffix.length) !== -1; -}; + String.isNullOrWhitespace = function( input ) { + return !input || !input.trim(); + }; -String.is = function( val ) { - return typeof val === 'string' || val instanceof String; -}; + String.prototype.endsWith = function(suffix) { + return this.indexOf(suffix, this.length - suffix.length) !== -1; + }; + + String.is = function( val ) { + return typeof val === 'string' || val instanceof String; + }; + +}()); diff --git a/src/verbs/convert.js b/src/verbs/convert.js index b6c1239..7386072 100644 --- a/src/verbs/convert.js +++ b/src/verbs/convert.js @@ -1,6 +1,6 @@ (function(){ - var loadSourceResumes = require('../core/load-source-resumes'); + var ResumeFactory = require('../core/resume-factory'); /** Convert between FRESH and JRS formats. @@ -16,7 +16,7 @@ if( src && dst && src.length && dst.length && src.length !== dst.length ) { throw { fluenterror: 7 }; } - var sheets = loadSourceResumes( src, _log ); + var sheets = ResumeFactory.load( src, _log ); sheets.forEach(function(sheet, idx){ var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; diff --git a/src/verbs/generate.js b/src/verbs/generate.js index d6629d2..2291f53 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -1,11 +1,13 @@ (function() { var PATH = require('path') + , FS = require('fs') , parsePath = require('parse-filepath') + , MD = require('marked') , MKDIRP = require('mkdirp') , _opts = require('../core/default-options') , FluentTheme = require('../core/theme') - , loadSourceResumes = require('../core/load-source-resumes') + , ResumeFactory = require('../core/resume-factory') , _ = require('underscore') , _fmts = require('../core/default-formats') , _err, _log, rez; @@ -36,9 +38,28 @@ _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; _opts.prettify = opts.prettify === true ? _opts.prettify : false; + // Verify the specified theme name/path + var relativeThemeFolder = '../../node_modules/fluent-themes/themes'; + var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme); + var exists = require('path-exists').sync; + if( !exists( tFolder ) ) { + tFolder = PATH.resolve( _opts.theme ); + if (!exists( tFolder )) { + throw { fluenterror: 1, data: _opts.theme }; + } + } + + // Load the theme + var theTheme = (new FluentTheme()).open( tFolder ); + _opts.themeObj = theTheme; + var numFormats = theTheme.formats ? Object.keys(theTheme.formats).length : 2; + _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + + (' theme (' + numFormats + ' formats)').info); + // Load input resumes... if( !src || !src.length ) { throw { fluenterror: 3 }; } - var sheets = loadSourceResumes( src, _log ); + var sheets = ResumeFactory.load( src, _log, null, + theTheme.render ? 'JRS' : 'FRESH' ); // Merge input resumes... var msg = ''; @@ -49,23 +70,6 @@ }); msg && _log(msg); - // Verify the specified theme name/path - var relativeThemeFolder = '../../node_modules/fluent-themes/themes'; - var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme); - var exists = require('path-exists').sync; - if (!exists( tFolder )) { - tFolder = PATH.resolve( _opts.theme ); - if (!exists( tFolder )) { - throw { fluenterror: 1, data: _opts.theme }; - } - } - - // Load the theme - var theTheme = (new FluentTheme()).open( tFolder ); - _opts.themeObj = theTheme; - _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + - (' theme (' + Object.keys(theTheme.formats).length + ' formats)').info); - // Expand output resumes... (can't use map() here) var targets = [], that = this; ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { @@ -74,11 +78,15 @@ pa = parsePath(to), fmat = pa.extname || '.all'; - targets.push.apply(targets, fmat === '.all' ? + targets.push.apply( + targets, fmat === '.all' ? + Object.keys( theTheme.formats ).map(function(k){ var z = theTheme.formats[k]; return { file: to.replace(/all$/g,z.outFormat), fmt: z }; - }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); + }) : + + [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); }); @@ -95,6 +103,11 @@ "/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt". */ function single( targInfo, theme ) { + + function MDIN(txt) { + return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); + } + try { var f = targInfo.file , fType = targInfo.fmt.outFormat @@ -113,22 +126,6 @@ function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; theFormat.gen.generate( rez, f, _opts ); - - // targInfo.fmt.files.forEach( function( form ) { - // - // if( form.action === 'transform' ) { - // var theFormat = _fmts.filter( function( fmt ) { - // return fmt.name === targInfo.fmt.outFormat; - // })[0]; - // MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; - // theFormat.gen.generate( rez, f, _opts ); - // } - // else if( form.action === null ) { - // // Copy the file - // } - // - // }); - } // Otherwise the theme has no files section else { @@ -138,8 +135,44 @@ theFormat = _fmts.filter( function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; - MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; - theFormat.gen.generate( rez, f, _opts ); + + var outFolder = PATH.dirname( f ); + MKDIRP.sync( outFolder ); // Ensure dest folder exists; + + // TODO: refactor + if( theme.render ) { + var COPY = require('copy'); + var globs = [ /*'**',*/ '*.css', '*.js', '*.png', '*.jpg', '*.gif', '*.bmp' ]; + COPY.sync( globs , outFolder, { + cwd: theme.folder, nodir: true, + ignore: ['node_modules/','node_modules/**'] + // rewrite: function(p1, p2) { + // return PATH.join(p2, p1); + // } + }); + + // Prevent JSON Resume theme .js from chattering + var consoleLog = console.log; + console.log = function() { }; + + // Call the theme's render method + var rezDupe = rez.harden(); + var rezHtml = theme.render( rezDupe ); + + // Turn logging back on + console.log = consoleLog; + + // Unharden + rezHtml = rezHtml.replace( /@@@@~.+?~@@@@/g, function(val){ + return MDIN( val.replace( /~@@@@/gm,'' ).replace( /@@@@~/gm,'' ) ); + }); + + // Save the file + FS.writeFileSync( f, rezHtml ); + } + else { + theFormat.gen.generate( rez, f, _opts ); + } } } catch( ex ) { diff --git a/src/verbs/validate.js b/src/verbs/validate.js index 52bd6da..239576c 100644 --- a/src/verbs/validate.js +++ b/src/verbs/validate.js @@ -1,7 +1,7 @@ (function() { var FS = require('fs'); - var loadSourceResumes = require('../core/load-source-resumes'); + var ResumeFactory = require('../core/resume-factory'); module.exports = @@ -20,7 +20,7 @@ }; // Load input resumes... - var sheets = loadSourceResumes(src, _log, function( res ) { + var sheets = ResumeFactory.load(src, _log, function( res ) { try { return { file: res, diff --git a/test/test-cli.js b/test/test-cli.js index 93be452..7c43fc9 100644 --- a/test/test-cli.js +++ b/test/test-cli.js @@ -43,15 +43,15 @@ describe('Testing CLI interface', function () { silent: true }; - run( 'new', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' ); - run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' ); - run( 'new', ['tests/sandbox/new-1.json', 'tests/sandbox/new-2.json', 'tests/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' ); - run( 'new', ['tests/sandbox/new-jrs-1.json', 'tests/sandbox/new-jrs-2.json', 'tests/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' ); - run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' ); + run( 'new', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' ); + run( 'new', ['test/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' ); + run( 'new', ['test/sandbox/new-1.json', 'test/sandbox/new-2.json', 'test/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' ); + run( 'new', ['test/sandbox/new-jrs-1.json', 'test/sandbox/new-jrs-2.json', 'test/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' ); + run( 'new', ['test/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' ); fail( 'new', [], [], opts, " (when a filename isn't specified)" ); run( 'validate', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], [], opts, ' (FRESH format)' ); - run( 'validate', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' ); + run( 'validate', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' ); function run( verb, src, dst, opts, msg ) { msg = msg || '.'; diff --git a/test/test-fresh-sheet.js b/test/test-fresh-sheet.js index 055931a..4d28559 100644 --- a/test/test-fresh-sheet.js +++ b/test/test-fresh-sheet.js @@ -43,13 +43,13 @@ describe('jane-doe.json (FRESH)', function () { it('should save without throwing an exception', function(){ function trySave() { - _sheet.save( 'tests/sandbox/jane-q-fullstacker.json' ); + _sheet.save( 'test/sandbox/jane-q-fullstacker.json' ); } trySave.should.not.Throw(); }); it('should not be modified after saving', function() { - var savedSheet = new FRESHResume().open('tests/sandbox/jane-q-fullstacker.json'); + var savedSheet = new FRESHResume().open('test/sandbox/jane-q-fullstacker.json'); _sheet.stringify().should.equal( savedSheet.stringify() ); }); diff --git a/test/test-jrs-sheet.js b/test/test-jrs-sheet.js index 86bb7ab..ec0a051 100644 --- a/test/test-jrs-sheet.js +++ b/test/test-jrs-sheet.js @@ -38,13 +38,13 @@ function testResume( opts ) { it('should save without throwing an exception', function() { var that = this; function trySave() { - _sheet.save( 'tests/sandbox/' + opts.title + '.json' ); + _sheet.save( 'test/sandbox/' + opts.title + '.json' ); } trySave.should.not.Throw(); }); it('should not be modified after saving', function() { - var savedSheet = new JRSResume().open( 'tests/sandbox/' + opts.title + '.json' ); + var savedSheet = new JRSResume().open( 'test/sandbox/' + opts.title + '.json' ); _sheet.stringify().should.equal( savedSheet.stringify() ); }); diff --git a/test/test-themes.js b/test/test-themes.js index 9d27120..5a6ee1d 100644 --- a/test/test-themes.js +++ b/test/test-themes.js @@ -1,5 +1,6 @@ -var chai = require('chai') +var SPAWNWATCHER = require('../src/core/spawn-watch') + , chai = require('chai') , expect = chai.expect , should = chai.should() , path = require('path') @@ -26,28 +27,47 @@ describe('Testing themes', function () { useful: 'green', }); - function genTheme( themeName ) { - it( themeName.toUpperCase() + ' theme should generate without throwing an exception', function () { + function genTheme( fmt, src, themeName, themeLoc, testTitle ) { + themeLoc = themeLoc || themeName; + testTitle = themeName.toUpperCase() + ' theme should generate without throwing an exception'; + it( testTitle, function () { function tryOpen() { - var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; - var dst = ['tests/sandbox/' + themeName + '/resume.all']; + //var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; + var dst = ['test/sandbox/' + fmt + '/' + themeName + '/resume.all']; var opts = { - theme: themeName, - format: 'FRESH', + theme: themeLoc, + format: fmt, prettify: true, silent: true }; - FCMD.verbs.build( src, dst, opts, function() { } ); + FCMD.verbs.build( src, dst, opts, function() {} ); } tryOpen.should.not.Throw(); }); } - genTheme('hello-world'); - genTheme('compact'); - genTheme('modern'); - genTheme('minimist'); - genTheme('awesome'); - genTheme('positive'); + var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; + genTheme('FRESH', src, 'hello-world'); + genTheme('FRESH', src, 'compact'); + genTheme('FRESH', src, 'modern'); + genTheme('FRESH', src, 'minimist'); + genTheme('FRESH', src, 'awesome'); + genTheme('FRESH', src, 'positive'); + genTheme('FRESH', src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' ); + genTheme('FRESH', src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' ); + genTheme('FRESH', src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' ); + genTheme('FRESH', src, 'jsonresume-theme-classy', 'node_modules/jsonresume-theme-classy' ); + + src = ['test/resumes/jrs-0.0.0/richard-hendriks.json']; + genTheme('JRS', src, 'hello-world'); + genTheme('JRS', src, 'compact'); + genTheme('JRS', src, 'modern'); + genTheme('JRS', src, 'minimist'); + genTheme('JRS', src, 'awesome'); + genTheme('JRS', src, 'positive'); + genTheme('JRS', src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' ); + genTheme('JRS', src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' ); + genTheme('JRS', src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' ); + genTheme('JRS', src, 'jsonresume-theme-classy', 'node_modules/jsonresume-theme-classy' ); });