From 0514f7805c359a885b958ec15404ac609a9ea694 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Sat, 26 Dec 2015 22:47:39 -0500 Subject: [PATCH 01/39] Add contributors to package.json. Contributors first, author last. --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 025892b..9ab84f5 100644 --- a/package.json +++ b/package.json @@ -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": { From ec511483748f3a1df61b438854cc699ae7c5784f Mon Sep 17 00:00:00 2001 From: hacksalot Date: Sun, 27 Dec 2015 00:08:45 -0500 Subject: [PATCH 02/39] Introduce interim contribution guidelines. --- CONTRIBUTING.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 7 ++++++ 2 files changed, 65 insertions(+) create mode 100644 CONTRIBUTING.md 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/README.md b/README.md index 141d7d8..9e35161 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,12 @@ hackmyresume generate resume.json -o someFile.all -s hackmyresume generate 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 +313,4 @@ 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 From 07b303e53000c98971b9523e9f3d1468cbb6ce07 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 28 Dec 2015 03:51:39 -0500 Subject: [PATCH 03/39] Bump version to 1.3.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ab84f5..1ac5aca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hackmyresume", - "version": "1.2.2", + "version": "1.3.0", "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", From 9423a1984236147edbafad289071e8aa133d59ec Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 28 Dec 2015 04:01:30 -0500 Subject: [PATCH 04/39] Remove extraneous references to "tests" plural. --- Gruntfile.js | 4 ++-- test/test-cli.js | 12 ++++++------ test/test-fresh-sheet.js | 4 ++-- test/test-jrs-sheet.js | 4 ++-- test/test-themes.js | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 45cd6ca..3e6d20a 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: { 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..44fd143 100644 --- a/test/test-themes.js +++ b/test/test-themes.js @@ -30,7 +30,7 @@ describe('Testing themes', function () { it( themeName.toUpperCase() + ' theme should generate without throwing an exception', function () { function tryOpen() { var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; - var dst = ['tests/sandbox/' + themeName + '/resume.all']; + var dst = ['test/sandbox/' + themeName + '/resume.all']; var opts = { theme: themeName, format: 'FRESH', From db31744c983b4d2c5bd34b8abfe581d802b9e175 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 28 Dec 2015 04:16:53 -0500 Subject: [PATCH 05/39] Adjust "npm test" command. Fix issue with tests being run twice and run tests through Grunt for LINTing and other pre/post processing. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ac5aca..fceeae0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/hacksalot/HackMyResume.git" }, "scripts": { - "test": "mocha && grunt", + "test": "grunt test", "grunt": "grunt" }, "keywords": [ From 547b87afc6d7f89ddab1fdb4f01ef66d6de2aa7f Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 28 Dec 2015 04:17:48 -0500 Subject: [PATCH 06/39] LINT prior to running tests. --- Gruntfile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 3e6d20a..ae15fac 100644 --- a/Gruntfile.js +++ b/Gruntfile.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' ]); }; From 3c1ae4cbd1e6dc1bde1459e1995a05fef697d00f Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 28 Dec 2015 04:37:42 -0500 Subject: [PATCH 07/39] Add baseline support for local generation of JSON Resume themes. --- package.json | 1 + src/core/fresh-resume.js | 18 ++++---- src/core/jrs-resume.js | 8 ++++ src/core/load-source-resumes.js | 13 ------ src/core/resume-factory.js | 43 +++++++++++++++++++ src/core/theme.js | 25 ++++++++--- src/verbs/convert.js | 4 +- src/verbs/generate.js | 73 ++++++++++++++++----------------- src/verbs/validate.js | 4 +- 9 files changed, 122 insertions(+), 67 deletions(-) delete mode 100644 src/core/load-source-resumes.js create mode 100644 src/core/resume-factory.js diff --git a/package.json b/package.json index fceeae0..ba19457 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,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", "wkhtmltopdf": "^0.1.5", "xml-escape": "^1.0.0", 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..619e83a 100644 --- a/src/core/jrs-resume.js +++ b/src/core/jrs-resume.js @@ -70,6 +70,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 +78,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 ) { 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/theme.js b/src/core/theme.js index 3b5f729..6e8356a 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/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..0061ab2 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -1,11 +1,12 @@ (function() { var PATH = require('path') + , FS = require('fs') , parsePath = require('parse-filepath') , 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 +37,27 @@ _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 +68,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 +76,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) ) }]); }); @@ -113,22 +119,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 { @@ -139,7 +129,14 @@ 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 ); + + if( theme.render ) { + var rezHtml = theme.render( rez ); + 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, From 6bc6b3262e8ca9c93ac2ae83dc60502269d32b47 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Mon, 28 Dec 2015 04:39:03 -0500 Subject: [PATCH 08/39] Add tests for FRESH/JRS cross-generation. Ability to generate JSON Resume themes from FRESH format resumes and vice-versa. --- package.json | 4 ++++ test/test-themes.js | 45 ++++++++++++++++++++++++++++++++------------- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index ba19457..aa96010 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,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/test/test-themes.js b/test/test-themes.js index 44fd143..8d195b3 100644 --- a/test/test-themes.js +++ b/test/test-themes.js @@ -26,28 +26,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 = ['test/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' ); }); From bf5c040971382b253a5d5ae04f3e68b04781c1d4 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Tue, 29 Dec 2015 03:10:26 -0500 Subject: [PATCH 09/39] Copy JRS theme assets to target. --- package.json | 1 + src/verbs/generate.js | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index aa96010..36504f8 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "homepage": "https://github.com/hacksalot/HackMyResume", "dependencies": { "colors": "^1.1.2", + "copy": "^0.1.3", "fluent-themes": "~0.7.1-beta", "fresca": "~0.2.2", "fs-extra": "^0.24.0", diff --git a/src/verbs/generate.js b/src/verbs/generate.js index 0061ab2..5c07ee9 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -57,7 +57,8 @@ // Load input resumes... if( !src || !src.length ) { throw { fluenterror: 3 }; } - var sheets = ResumeFactory.load( src, _log, null, theTheme.render ? 'JRS' : 'FRESH' ); + var sheets = ResumeFactory.load( src, _log, null, + theTheme.render ? 'JRS' : 'FRESH' ); // Merge input resumes... var msg = ''; @@ -128,9 +129,21 @@ theFormat = _fmts.filter( function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; - MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; + 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); + // } + }); var rezHtml = theme.render( rez ); FS.writeFileSync( f, rezHtml ); } From e65c0e128e83dc41050681180bb44259c1899f6d Mon Sep 17 00:00:00 2001 From: hacksalot Date: Tue, 29 Dec 2015 03:50:00 -0500 Subject: [PATCH 10/39] Fix tests glitch. --- Gruntfile.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index ae15fac..536fe8d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -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'] } }; diff --git a/package.json b/package.json index 36504f8..6658c7f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/hacksalot/HackMyResume.git" }, "scripts": { - "test": "grunt test", + "test": "grunt clean && mocha", "grunt": "grunt" }, "keywords": [ From 13430bcad52e52c36d6591cfb57287b95f6b6f62 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Tue, 29 Dec 2015 05:09:05 -0500 Subject: [PATCH 11/39] Refactor status codes. --- src/core/status-codes.js | 21 ++++++++++++++++ src/gen/base-generator.js | 9 +------ src/index.js | 50 +++++++++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 src/core/status-codes.js 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/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/index.js b/src/index.js index 5b65b71..07081da 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ var ARGS = require( 'minimist' ) , 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'); @@ -99,20 +100,49 @@ function handleError( ex ) { 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 + + + 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; + }).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; + + 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; + } exitCode = ex.fluenterror; From 02ef2b2241a1c42d1f3ac8fdd38f74814c20f35f Mon Sep 17 00:00:00 2001 From: hacksalot Date: Tue, 29 Dec 2015 06:35:55 -0500 Subject: [PATCH 12/39] Improve error handling. Better support for spawn errors encountered during generation (for ex, PDFs through wkhtml) + general refactoring. --- src/core/error-handler.js | 93 +++++++++++++++++++++++++++++++++++ src/core/spawn-watch.js | 22 +++++++++ src/gen/html-pdf-generator.js | 76 +++++++++++++++------------- src/gen/template-generator.js | 2 +- src/index.js | 83 +++---------------------------- test/test-themes.js | 5 +- 6 files changed, 169 insertions(+), 112 deletions(-) create mode 100644 src/core/error-handler.js create mode 100644 src/core/spawn-watch.js 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/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/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 07081da..48d2503 100644 --- a/src/index.js +++ b/src/index.js @@ -2,11 +2,14 @@ /** 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') @@ -19,11 +22,12 @@ var ARGS = require( 'minimist' ) + try { main(); } catch( ex ) { - handleError( ex ); + require('./core/error-handler').err( ex, true ); } @@ -91,76 +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 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; - - } - 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/test/test-themes.js b/test/test-themes.js index 8d195b3..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') @@ -64,7 +65,7 @@ describe('Testing themes', function () { 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-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' ); From 483207e5a084d41a9f3074edc7712c3c842855ea Mon Sep 17 00:00:00 2001 From: hacksalot Date: Tue, 29 Dec 2015 10:01:45 -0500 Subject: [PATCH 13/39] Improve Markdown support for JSON Resume themes. --- src/core/jrs-resume.js | 65 ++++++++++++++++++++++++++++++++++++++++++ src/verbs/generate.js | 25 +++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js index 619e83a..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'); /** @@ -238,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/verbs/generate.js b/src/verbs/generate.js index 5c07ee9..2291f53 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -3,6 +3,7 @@ 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') @@ -102,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 @@ -144,7 +150,24 @@ // return PATH.join(p2, p1); // } }); - var rezHtml = theme.render( rez ); + + // 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 { From b803eba934f1481d94299f6cdf691f1d86f2c558 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Tue, 29 Dec 2015 10:26:30 -0500 Subject: [PATCH 14/39] Scrub string.js. Will probably be retired in favor of Node reusables. --- src/utils/string.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) 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; + }; + +}()); From 7593afa58630d9f0607615c3b53337d2bceffa59 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Tue, 29 Dec 2015 17:33:16 -0500 Subject: [PATCH 15/39] Adjust package.json versions. Relax to v1.3.0-beta and bump fluent-themes version. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6658c7f..3786e9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hackmyresume", - "version": "1.3.0", + "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", @@ -47,7 +47,7 @@ "dependencies": { "colors": "^1.1.2", "copy": "^0.1.3", - "fluent-themes": "~0.7.1-beta", + "fluent-themes": "~0.8.0-beta", "fresca": "~0.2.2", "fs-extra": "^0.24.0", "handlebars": "^4.0.5", From 0c570f8512e693441f33d49d355da890e0e740ab Mon Sep 17 00:00:00 2001 From: hacksalot Date: Tue, 29 Dec 2015 17:43:27 -0500 Subject: [PATCH 16/39] Update README. --- README.md | 95 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 9e35161..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,8 +297,8 @@ 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 @@ -314,3 +327,5 @@ MIT. Go crazy. See [LICENSE.md][1] for details. [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 From b21fd93d66731db3fc5c19ce2a12b3734f4d4cf7 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 12:08:46 -0500 Subject: [PATCH 17/39] Introduce JRSTheme class. Start splitting out logic into dedicated abstractions for both FRESH and JSON Resume themes given the different structure and use cases of each. --- src/core/jrs-theme.js | 70 ++++++++++++++++++++++++++++++++++++++++ src/core/status-codes.js | 4 ++- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/core/jrs-theme.js diff --git a/src/core/jrs-theme.js b/src/core/jrs-theme.js new file mode 100644 index 0000000..26904c2 --- /dev/null +++ b/src/core/jrs-theme.js @@ -0,0 +1,70 @@ +/** +Definition of the JRSTheme class. +@module jrs-theme.js +@license MIT. See LICENSE.MD for details. +*/ + +(function() { + + + + var _ = require('underscore') + , PATH = require('path') + , parsePath = require('parse-filepath') + , pathExists = require('path-exists').sync; + + + + /** + The JRSTheme class is a representation of a JSON Resume theme asset. + @class JRSTheme + */ + function JRSTheme() { + + } + + /** + Open and parse the specified theme. + */ + JRSTheme.prototype.open = function( themeFolder ) { + + this.folder = themeFolder; + + // Open the [theme-name].json file; should have the same name as folder + var pathInfo = parsePath( themeFolder ); + + // Open and parse the theme's package.json file. + 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' } + }; + } + else { + throw { fluenterror: 10 }; + } + + return this; + }; + + /** + Determine if the theme supports the specified output format. + */ + JRSTheme.prototype.hasFormat = function( fmt ) { + return _.has( this.formats, fmt ); + }; + + /** + Determine if the theme supports the specified output format. + */ + JRSTheme.prototype.getFormat = function( fmt ) { + return this.formats[ fmt ]; + }; + + module.exports = JRSTheme; + +}()); diff --git a/src/core/status-codes.js b/src/core/status-codes.js index d0467c7..400467f 100644 --- a/src/core/status-codes.js +++ b/src/core/status-codes.js @@ -1,6 +1,7 @@ /** Status codes for HackMyResume. @module status-codes.js +@license MIT. See LICENSE.MD for details. */ (function(){ @@ -15,7 +16,8 @@ Status codes for HackMyResume. resumeNotFoundAlt: 6, inputOutputParity: 7, createNameMissing: 8, - wkhtmltopdf: 9 + wkhtmltopdf: 9, + missingPackageJSON: 10 }; }()); From 3e7d9c04112d613e6db2b0ce7f8e329f7dd9ea20 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 12:37:26 -0500 Subject: [PATCH 18/39] Integrate JRSTheme class. --- src/core/{theme.js => fresh-theme.js} | 0 src/gen/template-generator.js | 27 ++++++++++++++++++--------- src/hackmyapi.js | 3 ++- src/verbs/generate.js | 6 +++++- 4 files changed, 25 insertions(+), 11 deletions(-) rename src/core/{theme.js => fresh-theme.js} (100%) diff --git a/src/core/theme.js b/src/core/fresh-theme.js similarity index 100% rename from src/core/theme.js rename to src/core/fresh-theme.js diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 60a7a6f..20c3ea6 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -17,7 +17,8 @@ Definition of the TemplateGenerator class. , MKDIRP = require('mkdirp') , BaseGenerator = require( './base-generator' ) , EXTEND = require('../utils/extend') - , Theme = require('../core/theme'); + , FRESHTheme = require('../core/fresh-theme') + , JRSTheme = require('../core/jrs-theme'); @@ -220,20 +221,28 @@ Definition of the TemplateGenerator class. Given a theme title, load the corresponding theme. */ function themeFromMoniker() { + // Verify the specified theme name/path var tFolder = PATH.join( parsePath( require.resolve('fluent-themes') ).dirname, this.opts.theme ); - var exists = require('path-exists').sync; - if( !exists( tFolder ) ) { - tFolder = PATH.resolve( this.opts.theme ); - if( !exists( tFolder ) ) { - throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme}; - } - } - var t = this.opts.themeObj || new Theme().open( tFolder ); + var t; + if( this.opts.theme.startsWith('jsonresume-theme-') ) { + console.log('LOADING JSON RESUME'); + t = new JRSTheme().open( tFolder ); + } + else { + var exists = require('path-exists').sync; + if( !exists( tFolder ) ) { + tFolder = PATH.resolve( this.opts.theme ); + if( !exists( tFolder ) ) { + throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme}; + } + } + t = this.opts.themeObj || new FRESHTheme().open( tFolder ); + } // Load the theme and format return { diff --git a/src/hackmyapi.js b/src/hackmyapi.js index f36c40a..a118028 100644 --- a/src/hackmyapi.js +++ b/src/hackmyapi.js @@ -8,7 +8,8 @@ module.exports = { Sheet: require('./core/fresh-resume'), FRESHResume: require('./core/fresh-resume'), JRSResume: require('./core/jrs-resume'), - Theme: require('./core/theme'), + FRESHTheme: require('./core/fresh-theme'), + JRSTheme: require('./core/jrs-theme'), FluentDate: require('./core/fluent-date'), HtmlGenerator: require('./gen/html-generator'), TextGenerator: require('./gen/text-generator'), diff --git a/src/verbs/generate.js b/src/verbs/generate.js index 2291f53..344a9ba 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -6,10 +6,12 @@ , MD = require('marked') , MKDIRP = require('mkdirp') , _opts = require('../core/default-options') - , FluentTheme = require('../core/theme') + , FluentTheme = require('../core/fresh-theme') + , JRSTheme = require('../core/jrs-theme') , ResumeFactory = require('../core/resume-factory') , _ = require('underscore') , _fmts = require('../core/default-formats') + , unused = require('string.prototype.startswith') , _err, _log, rez; /** @@ -51,6 +53,8 @@ // Load the theme var theTheme = (new FluentTheme()).open( tFolder ); + var theTheme = _opts.theme.startsWith('jsonresume-theme') ? + new JRSTheme().open(tFolder) : new FluentTheme().open( tFolder ); _opts.themeObj = theTheme; var numFormats = theTheme.formats ? Object.keys(theTheme.formats).length : 2; _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + From 3c7868a750102c3475d392ee9a7166de6f9ff6dd Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 12:38:01 -0500 Subject: [PATCH 19/39] Scrub. --- src/gen/base-generator.js | 2 +- src/verbs/generate.js | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/gen/base-generator.js b/src/gen/base-generator.js index 5d11777..b8a9538 100644 --- a/src/gen/base-generator.js +++ b/src/gen/base-generator.js @@ -1,7 +1,7 @@ /** Definition of the BaseGenerator class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module base-generator.js +@license MIT. See LICENSE.md for details. */ (function() { diff --git a/src/verbs/generate.js b/src/verbs/generate.js index 344a9ba..f9f1a76 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -24,8 +24,8 @@ module.exports = /** - Given a source JSON resume, a destination resume path, and a theme file, - generate 0..N resumes in the desired formats. + Given a source resume in FRESH or JRS format, a destination resume path, and a + theme file, generate 0..N resumes in the desired formats. @param src Path to the source JSON resume file: "rez/resume.json". @param dst An array of paths to the target resume file(s). @param theme Friendly name of the resume theme. Defaults to "modern". @@ -52,7 +52,6 @@ } // Load the theme - var theTheme = (new FluentTheme()).open( tFolder ); var theTheme = _opts.theme.startsWith('jsonresume-theme') ? new JRSTheme().open(tFolder) : new FluentTheme().open( tFolder ); _opts.themeObj = theTheme; @@ -118,8 +117,8 @@ , fName = PATH.basename(f, '.' + fType) , theFormat; - // If targInfo.fmt.files exists, this theme has an explicit "files" - // section in its theme.json file. + // If targInfo.fmt.files exists, this format is backed by a document. + // Fluent/FRESH themes are handled here. if( targInfo.fmt.files && targInfo.fmt.files.length ) { _log( 'Generating '.useful + @@ -131,19 +130,22 @@ MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; theFormat.gen.generate( rez, f, _opts ); } - // Otherwise the theme has no files section + + // Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format + // (JSON, YML, or PNG) that every theme gets "for free". else { _log( 'Generating '.useful + targInfo.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold); - theFormat = _fmts.filter( - function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; + theFormat = _fmts.filter( function(fmt) { + return fmt.name === targInfo.fmt.outFormat; + })[0]; var outFolder = PATH.dirname( f ); MKDIRP.sync( outFolder ); // Ensure dest folder exists; - // TODO: refactor + // JSON Resume themes have a 'render' method that needs to be called if( theme.render ) { var COPY = require('copy'); var globs = [ /*'**',*/ '*.css', '*.js', '*.png', '*.jpg', '*.gif', '*.bmp' ]; @@ -155,7 +157,7 @@ // } }); - // Prevent JSON Resume theme .js from chattering + // Prevent JSON Resume theme .js from chattering (TODO: redirect IO) var consoleLog = console.log; console.log = function() { }; From a0c356941c20513db7808ca410d39a823194577b Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 12:44:16 -0500 Subject: [PATCH 20/39] Remove unnecessary line. --- src/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 48d2503..c48d960 100644 --- a/src/index.js +++ b/src/index.js @@ -74,10 +74,9 @@ function main() { var src = a._.slice(1, splitAt === -1 ? undefined : splitAt ); var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 ); ( splitAt === -1 ) && src.length > 1 && dst.push( src.pop() ); // Allow omitting TO keyword - var parms = [ src, dst, opts, logMsg ]; // Invoke the action - FCMD.verbs[ verb ].apply( null, parms ); + FCMD.verbs[ verb ].apply( null, [ src, dst, opts, logMsg ] ); } From e3cb949992561b1043452e792c9629ae10a25204 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 12:59:21 -0500 Subject: [PATCH 21/39] Fix: Exception when HMR is run without params. --- src/core/error-handler.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/error-handler.js b/src/core/error-handler.js index 53aa014..1bbc8ce 100644 --- a/src/core/error-handler.js +++ b/src/core/error-handler.js @@ -6,6 +6,9 @@ var HACKMYSTATUS = require('./status-codes') , PKG = require('../../package.json') + , FS = require('fs') + , FCMD = require('../hackmycmd') + , PATH = require('path') , title = ('\n*** HackMyResume v' + PKG.version + ' ***').bold.white; var ErrorHandler = module.exports = { @@ -36,7 +39,7 @@ 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; + FS.readFileSync( PATH.resolve(__dirname, '../use.txt'), 'utf8' ).info.bold; break; case HACKMYSTATUS.invalidCommand: From 1bcc2f7d0cc0eb8d3ee204b9498eeaa7af6363aa Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 13:00:30 -0500 Subject: [PATCH 22/39] Add formal support for aliases. new/create and build/generate --- src/hackmycmd.js | 20 ++++++++++++-------- src/index.js | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/hackmycmd.js b/src/hackmycmd.js index 0082f8e..e4449da 100644 --- a/src/hackmycmd.js +++ b/src/hackmycmd.js @@ -22,15 +22,19 @@ Internal resume generation logic for HackMyResume. /** Internal module interface. Used by FCV Desktop and HMR. */ + var v = { + build: require('./verbs/generate'), + validate: require('./verbs/validate'), + convert: require('./verbs/convert'), + new: require('./verbs/create'), + help: help + } + return { - verbs: { - generate: require('./verbs/generate'), - build: require('./verbs/generate'), - validate: require('./verbs/validate'), - convert: require('./verbs/convert'), - create: require('./verbs/create'), - new: require('./verbs/create'), - help: help + verbs: v, + alias: { + generate: v.build, + create: v.build }, lib: require('./hackmyapi'), options: require('./core/default-options'), diff --git a/src/index.js b/src/index.js index c48d960..d2f99cf 100644 --- a/src/index.js +++ b/src/index.js @@ -55,7 +55,7 @@ function main() { // Get the action to be performed var params = a._.map( function(p){ return p.toLowerCase().trim(); }); var verb = params[0]; - if( !FCMD.verbs[ verb ] ) { + if( !FCMD.verbs[ verb ] && !FCMD.alias[ verb ] ) { logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn); return; } @@ -76,7 +76,7 @@ function main() { ( splitAt === -1 ) && src.length > 1 && dst.push( src.pop() ); // Allow omitting TO keyword // Invoke the action - FCMD.verbs[ verb ].apply( null, [ src, dst, opts, logMsg ] ); + (FCMD.verbs[verb] || FCMD.alias[verb]).apply(null, [src, dst, opts, logMsg]); } From 7a1eadb3fc21a0ad822757fde62fec666a4fbbb3 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 13:12:51 -0500 Subject: [PATCH 23/39] Tweak error messages. Stay away from language like "please specify a valid input resume". The fluentcv fork can use corporate-speak. HackMyResume is more like a gremlin -- feed it, but never after midnight. --- src/core/error-handler.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/core/error-handler.js b/src/core/error-handler.js index 1bbc8ce..a2ade77 100644 --- a/src/core/error-handler.js +++ b/src/core/error-handler.js @@ -29,17 +29,21 @@ break; case HACKMYSTATUS.resumeNotFound: - msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + + msg = 'Please '.guide + 'feed me a 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) { + msg = title + "\nPlease ".guide + "give me a command".guide.bold + + " (".guide; + + msg += 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.resolve(__dirname, '../use.txt'), 'utf8' ).info.bold; + }).join(', '.guide) + ").\n\n".guide; + + msg += FS.readFileSync( + PATH.resolve(__dirname, '../use.txt'), 'utf8' ).info.bold; break; case HACKMYSTATUS.invalidCommand: @@ -48,7 +52,7 @@ break; case HACKMYSTATUS.resumeNotFoundAlt: - msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + + msg = 'Please '.guide + 'feed me a resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break; From 39d61c66b92a9fac1b4e4790923cac40a16155b3 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 13:22:18 -0500 Subject: [PATCH 24/39] Finish Theme --> FreshTheme rename. --- src/core/fresh-theme.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/fresh-theme.js b/src/core/fresh-theme.js index b9fe152..1d8fdc2 100644 --- a/src/core/fresh-theme.js +++ b/src/core/fresh-theme.js @@ -1,7 +1,7 @@ /** -Definition of the Theme class. -@license MIT. Copyright (c) 2015 hacksalot / FluentDesk. -@module theme.js +Definition of the FRESHTheme class. +@module fresh-theme.js +@license MIT. See LICENSE.md for details. */ (function() { @@ -18,17 +18,17 @@ Definition of the Theme class. , RECURSIVE_READ_DIR = require('recursive-readdir-sync'); /** - The Theme class is a representation of a HackMyResume theme asset. - @class Theme + The FRESHTheme class is a representation of a HackMyResume theme asset. + @class FRESHTheme */ - function Theme() { + function FRESHTheme() { } /** Open and parse the specified theme. */ - Theme.prototype.open = function( themeFolder ) { + FRESHTheme.prototype.open = function( themeFolder ) { this.folder = themeFolder; @@ -83,14 +83,14 @@ Definition of the Theme class. /** Determine if the theme supports the specified output format. */ - Theme.prototype.hasFormat = function( fmt ) { + FRESHTheme.prototype.hasFormat = function( fmt ) { return _.has( this.formats, fmt ); }; /** Determine if the theme supports the specified output format. */ - Theme.prototype.getFormat = function( fmt ) { + FRESHTheme.prototype.getFormat = function( fmt ) { return this.formats[ fmt ]; }; @@ -285,6 +285,6 @@ Definition of the Theme class. return friendly[val] || val; } - module.exports = Theme; + module.exports = FRESHTheme; }()); From 97c9ba08d002bed74322a2a124dc3a097d443fd5 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 14:00:09 -0500 Subject: [PATCH 25/39] Fix: Broken HELP command. --- src/hackmycmd.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/hackmycmd.js b/src/hackmycmd.js index e4449da..ad62e4e 100644 --- a/src/hackmycmd.js +++ b/src/hackmycmd.js @@ -8,15 +8,16 @@ Internal resume generation logic for HackMyResume. module.exports = function () { var unused = require('./utils/string') - , PATH = require('path'); + , PATH = require('path') + , FS = require('fs'); /** Display help documentation. */ function help() { - console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ) - .useful.bold ); + var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ); + console.log( manPage.useful.bold ); } /** @@ -28,7 +29,7 @@ Internal resume generation logic for HackMyResume. convert: require('./verbs/convert'), new: require('./verbs/create'), help: help - } + }; return { verbs: v, From 5e51beddf70d205aa25ca12ed0bbee774626e0b0 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 14:48:22 -0500 Subject: [PATCH 26/39] Refactor. --- src/gen/template-generator.js | 13 ++- src/verbs/generate.js | 161 ++++++++++++++++++++++------------ 2 files changed, 115 insertions(+), 59 deletions(-) diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 20c3ea6..8d3629c 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -1,6 +1,6 @@ /** Definition of the TemplateGenerator class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@license MIT. See LICENSE.md for details. @module template-generator.js */ @@ -78,7 +78,14 @@ Definition of the TemplateGenerator class. @method invoke @param rez A FreshResume object. @param opts Generator options. - @returns An array of strings representing generated output files. + @returns An array of objects representing the generated output files. Each + object has this format: + + { + files: [ { info: { }, data: [ ] }, { ... } ], + themeInfo: { } + } + */ invoke: function( rez, opts ) { @@ -183,6 +190,8 @@ Definition of the TemplateGenerator class. }); } + return genInfo; + }, diff --git a/src/verbs/generate.js b/src/verbs/generate.js index f9f1a76..276fdf4 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -1,19 +1,27 @@ +/** +Implementation of the 'generate' verb for HackMyResume. +@module generate.js +@license MIT. See LICENSE.md for details. +*/ + (function() { var PATH = require('path') , FS = require('fs') - , parsePath = require('parse-filepath') , MD = require('marked') , MKDIRP = require('mkdirp') + , EXTEND = require('../utils/extend') + , parsePath = require('parse-filepath') , _opts = require('../core/default-options') , FluentTheme = require('../core/fresh-theme') , JRSTheme = require('../core/jrs-theme') , ResumeFactory = require('../core/resume-factory') , _ = require('underscore') , _fmts = require('../core/default-formats') - , unused = require('string.prototype.startswith') , _err, _log, rez; + + /** Handle an exception. */ @@ -21,7 +29,7 @@ throw ex; } - module.exports = + /** Given a source resume in FRESH or JRS format, a destination resume path, and a @@ -31,33 +39,18 @@ @param theme Friendly name of the resume theme. Defaults to "modern". @param logger Optional logging override. */ - function generate( src, dst, opts, logger, errHandler ) { + function build( src, dst, opts, logger, errHandler ) { + // Housekeeping... _log = logger || console.log; _err = errHandler || error; - //_opts = extend( true, _opts, opts ); _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 = _opts.theme.startsWith('jsonresume-theme') ? - new JRSTheme().open(tFolder) : 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 the theme... + var tFolder = verify_theme( _opts.theme ); + var theTheme = load_theme( tFolder ); // Load input resumes... if( !src || !src.length ) { throw { fluenterror: 3 }; } @@ -73,41 +66,28 @@ }); msg && _log(msg); - // Expand output resumes... (can't use map() here) - var targets = [], that = this; - ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { - - var to = PATH.resolve(t), - pa = parsePath(to), - fmat = pa.extname || '.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) ) }]); - - }); + // Expand output resumes... + var targets = expand( dst, theTheme ); // Run the transformation! - var finished = targets.map( function(t) { return single(t, theTheme); }); + var finished = targets.map( function(t) { + return EXTEND(true, t, { markup: single(t, theTheme) }); + }); // Don't send the client back empty-handed return { sheet: rez, 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". + Generate a single resume of a specific format. TODO: Refactor. + @param targInfo Information for the target resume. + @param theme A FRESHTheme or JRSTheme object. */ function single( targInfo, theme ) { - function MDIN(txt) { + function MDIN(txt) { // TODO: Move this return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); } @@ -117,14 +97,13 @@ , fName = PATH.basename(f, '.' + fType) , theFormat; + _log( 'Generating '.useful + + targInfo.fmt.outFormat.toUpperCase().useful.bold + + ' resume: '.useful + PATH.relative(process.cwd(), f ).useful.bold ); + // If targInfo.fmt.files exists, this format is backed by a document. // Fluent/FRESH themes are handled here. if( targInfo.fmt.files && targInfo.fmt.files.length ) { - - _log( 'Generating '.useful + - targInfo.fmt.outFormat.toUpperCase().useful.bold + - ' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold); - theFormat = _fmts.filter( function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; @@ -134,9 +113,6 @@ // Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format // (JSON, YML, or PNG) that every theme gets "for free". else { - _log( 'Generating '.useful + - targInfo.fmt.outFormat.toUpperCase().useful.bold + - ' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold); theFormat = _fmts.filter( function(fmt) { return fmt.name === targInfo.fmt.outFormat; @@ -175,9 +151,12 @@ // Save the file FS.writeFileSync( f, rezHtml ); + + // Return markup to the client + return rezHtml; } else { - theFormat.gen.generate( rez, f, _opts ); + return theFormat.gen.generate( rez, f, _opts ); } } } @@ -186,4 +165,72 @@ } } + + /** + Expand output files. + */ + function expand( dst, theTheme ) { + var targets = []; + // (can't use map() here). + ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { + + var to = PATH.resolve(t), + pa = parsePath(to), + fmat = pa.extname || '.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) ) }]); + + }); + return targets; + } + + + /** + Verify the specified theme name/path. + */ + function verify_theme( themeNameOrPath ) { + var tFolder = PATH.resolve( + __dirname, + '../../node_modules/fluent-themes/themes', + themeNameOrPath + ); + var exists = require('path-exists').sync; + if( !exists( tFolder ) ) { + tFolder = PATH.resolve( themeNameOrPath ); + if( !exists( tFolder ) ) { + throw { fluenterror: 1, data: _opts.theme }; + } + } + return tFolder; + } + + + + /** + Load the specified theme. + */ + function load_theme( tFolder ) { + var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ? + new JRSTheme().open(tFolder) : new FluentTheme().open( tFolder ); + _opts.themeObj = theTheme; + var numFormats = Object.keys(theTheme.formats).length; + _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + + (' theme (' + numFormats + ' formats)').info); + return theTheme; + } + + + + module.exports = build; + + + }()); From ccadb0416f8d82fbfe8e117acfd4a125494adf4c Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 15:03:26 -0500 Subject: [PATCH 27/39] Move freebie formats out of theme class. --- src/core/fresh-theme.js | 4 ---- src/verbs/generate.js | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core/fresh-theme.js b/src/core/fresh-theme.js index 1d8fdc2..bf2b414 100644 --- a/src/core/fresh-theme.js +++ b/src/core/fresh-theme.js @@ -67,10 +67,6 @@ Definition of the FRESHTheme class. formatsHash = loadImplicit.call( this ); } - // Add freebie formats every theme gets - formatsHash.json = { title: 'json', outFormat: 'json', pre: 'json', ext: 'json', path: null, data: null }; - formatsHash.yml = { title: 'yaml', outFormat: 'yml', pre: 'yml', ext: 'yml', path: null, data: null }; - // Cache this.formats = formatsHash; diff --git a/src/verbs/generate.js b/src/verbs/generate.js index 276fdf4..c0cd8da 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -218,9 +218,25 @@ Implementation of the 'generate' verb for HackMyResume. Load the specified theme. */ function load_theme( tFolder ) { + + // Create a FRESH or JRS theme object var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ? new JRSTheme().open(tFolder) : new FluentTheme().open( tFolder ); + + // Add freebie formats every theme gets + theTheme.formats.json = theTheme.formats.json || { + title: 'json', outFormat: 'json', pre: 'json', + ext: 'json', path: null, data: null + }; + theTheme.formats.yml = theTheme.formats.yml || { + title: 'yaml', outFormat: 'yml', pre: 'yml', + ext: 'yml', path: null, data: null + }; + + // Cache the theme object _opts.themeObj = theTheme; + + // Output a message TODO: core should not log var numFormats = Object.keys(theTheme.formats).length; _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + (' theme (' + numFormats + ' formats)').info); From 2ff912e687d7f446b1c506d72c677a000cebee25 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 15:11:18 -0500 Subject: [PATCH 28/39] Scrub. --- src/core/jrs-theme.js | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/core/jrs-theme.js b/src/core/jrs-theme.js index 26904c2..ccf3300 100644 --- a/src/core/jrs-theme.js +++ b/src/core/jrs-theme.js @@ -16,32 +16,37 @@ Definition of the JRSTheme class. /** - The JRSTheme class is a representation of a JSON Resume theme asset. + The JRSTheme class is a representation of a JSON Resume + theme asset. @class JRSTheme */ function JRSTheme() { } + + /** Open and parse the specified theme. + @method open */ - JRSTheme.prototype.open = function( themeFolder ) { + JRSTheme.prototype.open = function( thFolder ) { - this.folder = themeFolder; + this.folder = thFolder; - // Open the [theme-name].json file; should have the same name as folder - var pathInfo = parsePath( themeFolder ); + // Open the [theme-name].json file; should have the same + // name as folder + var pathInfo = parsePath( thFolder ); // Open and parse the theme's package.json file. - 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; + var pkgJsonPath = PATH.join( thFolder, 'package.json' ); + if( pathExists( pkgJsonPath )) { + var thApi = require( thFolder ) + , thPkg = require( pkgJsonPath ); + this.name = thPkg.name; + this.render = (thApi && thApi.render) || undefined; this.formats = { - html: { title: 'html', outFormat: 'html', ext: 'html' } + html: { title:'html', outFormat:'html', ext:'html' } }; } else { @@ -51,20 +56,30 @@ Definition of the JRSTheme class. return this; }; + + /** - Determine if the theme supports the specified output format. + Determine if the theme supports the output format. + @method hasFormat */ JRSTheme.prototype.hasFormat = function( fmt ) { return _.has( this.formats, fmt ); }; + + /** - Determine if the theme supports the specified output format. + Return the requested output format. + @method getFormat */ JRSTheme.prototype.getFormat = function( fmt ) { return this.formats[ fmt ]; }; + + module.exports = JRSTheme; + + }()); From 72de1bbd335cb2176eb7f808bf22dc04af47a5f1 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 15:21:58 -0500 Subject: [PATCH 29/39] Scrub. --- src/core/fresh-theme.js | 91 +++++++++++++++++++++++++++-------------- src/core/jrs-theme.js | 2 +- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/core/fresh-theme.js b/src/core/fresh-theme.js index bf2b414..bbb816c 100644 --- a/src/core/fresh-theme.js +++ b/src/core/fresh-theme.js @@ -6,6 +6,8 @@ Definition of the FRESHTheme class. (function() { + + var FS = require('fs') , extend = require('../utils/extend') , validator = require('is-my-json-valid') @@ -15,16 +17,21 @@ Definition of the FRESHTheme class. , pathExists = require('path-exists').sync , EXTEND = require('../utils/extend') , moment = require('moment') - , RECURSIVE_READ_DIR = require('recursive-readdir-sync'); + , READFILES = require('recursive-readdir-sync'); + + /** - The FRESHTheme class is a representation of a HackMyResume theme asset. + The FRESHTheme class is a representation of a FRESH theme + asset. See also: JRSTheme. @class FRESHTheme */ function FRESHTheme() { } + + /** Open and parse the specified theme. */ @@ -38,18 +45,7 @@ Definition of the FRESHTheme class. // 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 + // Load the theme var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' ); var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) ); var that = this; @@ -76,6 +72,8 @@ Definition of the FRESHTheme class. return this; }; + + /** Determine if the theme supports the specified output format. */ @@ -83,6 +81,8 @@ Definition of the FRESHTheme class. return _.has( this.formats, fmt ); }; + + /** Determine if the theme supports the specified output format. */ @@ -90,6 +90,11 @@ Definition of the FRESHTheme class. return this.formats[ fmt ]; }; + + /** + Load the theme implicitly, by scanning the theme folder for + files. TODO: Refactor duplicated code with loadExplicit. + */ function loadImplicit() { // Set up a hash of formats supported by this theme. @@ -103,7 +108,7 @@ Definition of the FRESHTheme class. // Iterate over all files in the theme folder, producing an array, fmts, // containing info for each file. While we're doing that, also build up // the formatsHash object. - var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) { + var fmts = READFILES(tplFolder).map( function(absPath) { // If this file lives in a specific format folder within the theme, // such as "/latex" or "/html", then that format is the output format @@ -131,7 +136,7 @@ Definition of the FRESHTheme class. // compact-[outputformat].[extension], for ex, compact-pdf.html. if( !outFmt ) { var idx = pathInfo.name.lastIndexOf('-'); - outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 ); + outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1); isMajor = true; } @@ -161,9 +166,13 @@ Definition of the FRESHTheme class. }); // Now, get all the CSS files... - (this.cssFiles = fmts.filter(function( fmt ){ return fmt && (fmt.ext === 'css'); })) + (this.cssFiles = fmts.filter(function( fmt ){ + return fmt && (fmt.ext === 'css'); + })) + + // For each CSS file, get its corresponding HTML file .forEach(function( cssf ) { - // For each CSS file, get its corresponding HTML file + var idx = _.findIndex(fmts, function( fmt ) { return fmt && fmt.pre === cssf.pre && fmt.ext === 'html'; }); @@ -180,16 +189,19 @@ Definition of the FRESHTheme class. return formatsHash; } + + + /** + Load the theme explicitly, by following the 'formats' hash + in the theme's JSON settings file. + */ function loadExplicit() { - var that = this; - // Set up a hash of formats supported by this theme. + // Housekeeping var formatsHash = { }; - - // Establish the base theme folder var tplFolder = PATH.join( this.folder, 'src' ); - var act = null; + var that = this; // Iterate over all files in the theme folder, producing an array, fmts, // containing info for each file. While we're doing that, also build up @@ -200,13 +212,16 @@ Definition of the FRESHTheme class. // If this file is mentioned in the theme's JSON file under "transforms" var pathInfo = parsePath(absPath); var absPathSafe = absPath.trim().toLowerCase(); - var outFmt = _.find( Object.keys( that.formats ), function( fmtKey ) { - var fmtVal = that.formats[ fmtKey ]; - return _.some( fmtVal.transform, function( fpath ) { - var absPathB = PATH.join( that.folder, fpath ).trim().toLowerCase(); - return absPathB === absPathSafe; + var outFmt = _.find( + Object.keys( that.formats ), + function( fmtKey ) { + var fmtVal = that.formats[ fmtKey ]; + return _.some( fmtVal.transform, function(fpath) { + var absPathB = PATH.join( that.folder, fpath ) + .trim().toLowerCase(); + return absPathB === absPathSafe; + }); }); - }); if( outFmt ) { act = 'transform'; } @@ -227,7 +242,7 @@ Definition of the FRESHTheme class. // compact-[outputformat].[extension], for ex, compact-pdf.html. if( !outFmt ) { var idx = pathInfo.name.lastIndexOf('-'); - outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 ); + outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1); } // We should have a valid output format now. @@ -257,7 +272,11 @@ Definition of the FRESHTheme class. }); // Now, get all the CSS files... - (this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; })) + (this.cssFiles = fmts.filter(function( fmt ){ + return fmt.ext === 'css'; + })) + + // For each CSS file, get its corresponding HTML file .forEach(function( cssf ) { // For each CSS file, get its corresponding HTML file var idx = _.findIndex(fmts, function( fmt ) { @@ -275,12 +294,22 @@ Definition of the FRESHTheme class. return formatsHash; } + + + /** + Return a more friendly name for certain formats. + TODO: Refactor + */ function friendlyName( val ) { val = val.trim().toLowerCase(); var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }; return friendly[val] || val; } + + module.exports = FRESHTheme; + + }()); diff --git a/src/core/jrs-theme.js b/src/core/jrs-theme.js index ccf3300..4f2942a 100644 --- a/src/core/jrs-theme.js +++ b/src/core/jrs-theme.js @@ -17,7 +17,7 @@ Definition of the JRSTheme class. /** The JRSTheme class is a representation of a JSON Resume - theme asset. + theme asset. See also: FRESHTheme. @class JRSTheme */ function JRSTheme() { From 22554c61c5b749817acfd00b9dde9772e5b88f8d Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 18:18:11 -0500 Subject: [PATCH 30/39] Rename and bump fluent-themes dependency. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8eb6ba2..b3719d6 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "colors": "^1.1.2", "copy": "^0.1.3", - "fluent-themes": "~0.8.0-beta", + "fresh-themes": "~0.9.0-beta", "fresca": "~0.2.2", "fs-extra": "^0.24.0", "handlebars": "^4.0.5", From d4e0a0fa055c583e3a1f1b14582ddd23cce32df7 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 18:19:00 -0500 Subject: [PATCH 31/39] Add {{styleSheet}} helper (placeholder). --- src/eng/generic-helpers.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/eng/generic-helpers.js b/src/eng/generic-helpers.js index ab9c1f6..26c03dc 100644 --- a/src/eng/generic-helpers.js +++ b/src/eng/generic-helpers.js @@ -1,6 +1,6 @@ /** -Generic template helper definitions for FluentCV. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +Generic template helper definitions for HackMyResume / FluentCV. +@license MIT. See LICENSE.md for details. @module generic-helpers.js */ @@ -114,6 +114,14 @@ Generic template helper definitions for FluentCV. if (lhs || rhs) return options.fn(this); }, + /** + Conditional stylesheet link. Either display the link or embed the stylesheet + via tag. + */ + styleSheet: function( file ) { + return ''; + }, + /** Perform a generic comparison. See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates From d9010470439108df66b89fe470198e89ef87746d Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 18:50:58 -0500 Subject: [PATCH 32/39] Update fluent-themes --> fresh-themes. --- README.md | 2 +- src/gen/template-generator.js | 2 +- src/verbs/generate.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e58b005..4e0aa3b 100644 --- a/README.md +++ b/README.md @@ -327,5 +327,5 @@ MIT. Go crazy. See [LICENSE.md][1] for details. [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 +[fresh-themes]: https://github.com/fluentdesk/fresh-themes [jrst]: https://www.npmjs.com/search?q=jsonresume-theme diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 8d3629c..257f477 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -233,7 +233,7 @@ Definition of the TemplateGenerator class. // Verify the specified theme name/path var tFolder = PATH.join( - parsePath( require.resolve('fluent-themes') ).dirname, + parsePath( require.resolve('fresh-themes') ).dirname, this.opts.theme ); diff --git a/src/verbs/generate.js b/src/verbs/generate.js index c0cd8da..e4f06c9 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -199,7 +199,7 @@ Implementation of the 'generate' verb for HackMyResume. function verify_theme( themeNameOrPath ) { var tFolder = PATH.resolve( __dirname, - '../../node_modules/fluent-themes/themes', + '../../node_modules/fresh-themes/themes', themeNameOrPath ); var exists = require('path-exists').sync; From 558a321fe80bc39d02ce71653ef157c5a927220e Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 18:52:41 -0500 Subject: [PATCH 33/39] Refactor generator logic. --- package.json | 2 +- src/gen/html-png-generator.js | 31 +++++++------ src/verbs/generate.js | 84 ++++++++++++++++++++++++----------- 3 files changed, 77 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index b3719d6..79c689c 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "colors": "^1.1.2", "copy": "^0.1.3", - "fresh-themes": "~0.9.0-beta", + "fresh-themes": "~0.9.2-beta", "fresca": "~0.2.2", "fs-extra": "^0.24.0", "handlebars": "^4.0.5", diff --git a/src/gen/html-png-generator.js b/src/gen/html-png-generator.js index 4d9b5b9..2ec793e 100644 --- a/src/gen/html-png-generator.js +++ b/src/gen/html-png-generator.js @@ -1,6 +1,6 @@ /** Definition of the HtmlPngGenerator class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@license MIT. See LICENSE.MD for details. @module html-png-generator.js */ @@ -11,7 +11,7 @@ Definition of the HtmlPngGenerator class. , HTML = require( 'html' ); /** - An HTML-based PDF resume generator for HackMyResume. + An HTML-based PNG resume generator for HackMyResume. */ var HtmlPngGenerator = module.exports = TemplateGenerator.extend({ @@ -19,24 +19,29 @@ Definition of the HtmlPngGenerator class. this._super( 'png', 'html' ); }, - /** - Generate the binary PDF. - */ - onBeforeSave: function( info ) { - png( info.mk, info.outputFile ); - return null; // halt further processing + invoke: function( rez, themeMarkup, cssInfo, opts ) { + //return YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 ); + }, + + generate: function( rez, f, opts ) { + var htmlResults = opts.targets.filter(function(t){ + return t.fmt.outFormat === 'html'; + }); + var htmlFile = htmlResults[0].final.files.filter(function(fl){ + return fl.info.ext === 'html'; + }); + png(htmlFile[0].data, f); } }); /** - Generate a PDF from HTML. + Generate a PNG from HTML. */ function png( markup, fOut ) { - - require('webshot')( markup , { encoding: 'binary', siteType: 'html' } ) - .pipe( FS.createWriteStream( fOut ) ); - + // require('webshot')( markup , { encoding: 'binary', siteType: 'html' } ) + // .pipe( FS.createWriteStream( fOut ) ); + require('webshot')( markup , fOut, { siteType: 'html' }, function(err) { } ); } }()); diff --git a/src/verbs/generate.js b/src/verbs/generate.js index e4f06c9..eecdc9a 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -6,6 +6,8 @@ Implementation of the 'generate' verb for HackMyResume. (function() { + + var PATH = require('path') , FS = require('fs') , MD = require('marked') @@ -70,22 +72,23 @@ Implementation of the 'generate' verb for HackMyResume. var targets = expand( dst, theTheme ); // Run the transformation! - var finished = targets.map( function(t) { - return EXTEND(true, t, { markup: single(t, theTheme) }); + targets.forEach( function(t) { + t.final = single( t, theTheme, targets ); }); // Don't send the client back empty-handed - return { sheet: rez, targets: targets, processed: finished }; + return { sheet: rez, targets: targets, processed: targets }; } /** - Generate a single resume of a specific format. TODO: Refactor. + Generate a single target resume such as "out/rez.html" or "out/rez.doc". @param targInfo Information for the target resume. @param theme A FRESHTheme or JRSTheme object. + @returns */ - function single( targInfo, theme ) { + function single( targInfo, theme, finished ) { function MDIN(txt) { // TODO: Move this return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); @@ -107,7 +110,8 @@ Implementation of the 'generate' verb for HackMyResume. 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 ); + _opts.targets = finished; + return theFormat.gen.generate( rez, f, _opts ); } // Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format @@ -166,29 +170,67 @@ Implementation of the 'generate' verb for HackMyResume. } + /** - Expand output files. + Expand output files. For example, "foo.all" should be expanded to + ["foo.html", "foo.doc", "foo.pdf", "etc"]. + @param dst An array of output files as specified by the user. + @param theTheme A FRESHTheme or JRSTheme object. */ function expand( dst, theTheme ) { - var targets = []; - // (can't use map() here). - ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { - var to = PATH.resolve(t), - pa = parsePath(to), - fmat = pa.extname || '.all'; + // Add freebie formats (JSON, YAML, PNG) every theme gets... + // Add HTML-driven PNG only if the theme has an HTML format. + theTheme.formats.json = theTheme.formats.json || { + freebie: true, title: 'json', outFormat: 'json', pre: 'json', + ext: 'json', path: null, data: null + }; + theTheme.formats.yml = theTheme.formats.yml || { + freebie: true, title: 'yaml', outFormat: 'yml', pre: 'yml', + ext: 'yml', path: null, data: null + }; + theTheme.formats.png = theTheme.formats.png || + ( theTheme.formats.html && { + freebie: true, title: 'png', outFormat: 'png', + ext: 'yml', path: null, data: null + }); + + // Set up the destination collection. It's either the array of files passed + // by the user or 'out/resume.all' if no targets were specified. + var destColl = (dst && dst.length && dst) || + [PATH.normalize('out/resume.all')] + + // Assemble an array of expanded target files... (can't use map() here) + var targets = []; + destColl.forEach( function(t) { + + var to = PATH.resolve(t), pa = parsePath(to),fmat = pa.extname || '.all'; + + var explicitFormats = _.omit( theTheme.formats, function(val) { + return !val.freebie; + }); + var implicitFormats = _.omit( theTheme.formats, function(val) { + return val.freebie; + }); targets.push.apply( targets, fmat === '.all' ? - - Object.keys( theTheme.formats ).map(function(k){ + Object.keys( implicitFormats ).map( function( k ) { var z = theTheme.formats[k]; - return { file: to.replace(/all$/g,z.outFormat), fmt: z }; + return { file: to.replace( /all$/g, z.outFormat ), fmt: z }; }) : + [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); + targets.push.apply( + targets, fmat === '.all' ? + Object.keys( explicitFormats ).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) ) }]); }); + return targets; } @@ -223,16 +265,6 @@ Implementation of the 'generate' verb for HackMyResume. var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ? new JRSTheme().open(tFolder) : new FluentTheme().open( tFolder ); - // Add freebie formats every theme gets - theTheme.formats.json = theTheme.formats.json || { - title: 'json', outFormat: 'json', pre: 'json', - ext: 'json', path: null, data: null - }; - theTheme.formats.yml = theTheme.formats.yml || { - title: 'yaml', outFormat: 'yml', pre: 'yml', - ext: 'yml', path: null, data: null - }; - // Cache the theme object _opts.themeObj = theTheme; From a280d8acb22b089b0afa708142178bb7ddd7d86f Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 19:45:50 -0500 Subject: [PATCH 34/39] Support CSS embedding vs. linking. --- src/eng/generic-helpers.js | 6 ++++-- src/eng/handlebars-generator.js | 1 + src/index.js | 3 ++- src/verbs/generate.js | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/eng/generic-helpers.js b/src/eng/generic-helpers.js index 26c03dc..a930933 100644 --- a/src/eng/generic-helpers.js +++ b/src/eng/generic-helpers.js @@ -118,8 +118,10 @@ Generic template helper definitions for HackMyResume / FluentCV. Conditional stylesheet link. Either display the link or embed the stylesheet via tag. */ - styleSheet: function( file ) { - return ''; + styleSheet: function( file, options ) { + return ( this.opts.css === 'link') ? + '' : + ''; }, /** diff --git a/src/eng/handlebars-generator.js b/src/eng/handlebars-generator.js index 71dbf58..27a8f7d 100644 --- a/src/eng/handlebars-generator.js +++ b/src/eng/handlebars-generator.js @@ -40,6 +40,7 @@ Definition of the HandlebarsGenerator class. RAW: json, filt: opts.filters, cssInfo: cssInfo, + opts: opts, headFragment: opts.headFragment || '' }); diff --git a/src/index.js b/src/index.js index d2f99cf..9352d49 100644 --- a/src/index.js +++ b/src/index.js @@ -91,6 +91,7 @@ function getOpts( args ) { theme: args.t || 'modern', format: args.f || 'FRESH', prettify: !noPretty, - silent: args.s || args.silent + silent: args.s || args.silent, + css: args.css || 'embed' }; } diff --git a/src/verbs/generate.js b/src/verbs/generate.js index eecdc9a..d3819a7 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -49,6 +49,7 @@ Implementation of the 'generate' verb for HackMyResume. //_opts = extend( true, _opts, opts ); _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; _opts.prettify = opts.prettify === true ? _opts.prettify : false; + _opts.css = opts.css; // Load the theme... var tFolder = verify_theme( _opts.theme ); From 55943bf49a02317373475ac281e2c8f3cc92e1f9 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 20:09:39 -0500 Subject: [PATCH 35/39] Fix missing semicolon. --- src/verbs/generate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/verbs/generate.js b/src/verbs/generate.js index d3819a7..c8984d1 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -199,7 +199,7 @@ Implementation of the 'generate' verb for HackMyResume. // Set up the destination collection. It's either the array of files passed // by the user or 'out/resume.all' if no targets were specified. var destColl = (dst && dst.length && dst) || - [PATH.normalize('out/resume.all')] + [PATH.normalize('out/resume.all')]; // Assemble an array of expanded target files... (can't use map() here) var targets = []; From 76cafa42491b7157d982d4f37209aeecede1d0f0 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 20:10:14 -0500 Subject: [PATCH 36/39] Fix reference error in explicit themes. --- src/core/fresh-theme.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/fresh-theme.js b/src/core/fresh-theme.js index bbb816c..dc01975 100644 --- a/src/core/fresh-theme.js +++ b/src/core/fresh-theme.js @@ -206,7 +206,7 @@ Definition of the FRESHTheme class. // Iterate over all files in the theme folder, producing an array, fmts, // containing info for each file. While we're doing that, also build up // the formatsHash object. - var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) { + var fmts = READFILES( tplFolder ).map( function( absPath ) { act = null; // If this file is mentioned in the theme's JSON file under "transforms" @@ -275,7 +275,7 @@ Definition of the FRESHTheme class. (this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; })) - + // For each CSS file, get its corresponding HTML file .forEach(function( cssf ) { // For each CSS file, get its corresponding HTML file From 3acf648eb486218dd3fd8a46cdfa81c85363525e Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 20:11:09 -0500 Subject: [PATCH 37/39] Expose helpers to Underscore engine. Get the same set of helpers working for Underscore and Handlebars engines. Needs refactoring. --- src/eng/underscore-generator.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js index 0a9be84..eaa8eed 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -11,7 +11,6 @@ Definition of the UnderscoreGenerator class. var _ = require('underscore'); - /** Perform template-based resume generation using Underscore.js. @class UnderscoreGenerator @@ -32,6 +31,10 @@ Definition of the UnderscoreGenerator class. // Strip {# comments #} jst = jst.replace( delims.comment, ''); + var helpers = require('./generic-helpers'); + helpers.opts = opts; + helpers.cssInfo = cssInfo; + // Compile and run the template. TODO: avoid unnecessary recompiles. var compiled = _.template(jst); var ret = compiled({ @@ -40,13 +43,15 @@ Definition of the UnderscoreGenerator class. XML: require('xml-escape'), RAW: json, cssInfo: cssInfo, - headFragment: opts.headFragment || '' + headFragment: opts.headFragment || '', + opts: opts, + h: helpers }); return ret; } }; - + }()); From 3cf24cfb402a2cbaf977e5ee0a762af944159890 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 20:11:21 -0500 Subject: [PATCH 38/39] Fix PNG generation glitch. --- src/verbs/generate.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/verbs/generate.js b/src/verbs/generate.js index c8984d1..55011e3 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -190,11 +190,12 @@ Implementation of the 'generate' verb for HackMyResume. freebie: true, title: 'yaml', outFormat: 'yml', pre: 'yml', ext: 'yml', path: null, data: null }; - theTheme.formats.png = theTheme.formats.png || - ( theTheme.formats.html && { - freebie: true, title: 'png', outFormat: 'png', - ext: 'yml', path: null, data: null - }); + if( theTheme.formats.html && !theTheme.formats.png ) { + theTheme.formats.png = { + freebie: true, title: 'png', outFormat: 'png', + ext: 'yml', path: null, data: null + }; + } // Set up the destination collection. It's either the array of files passed // by the user or 'out/resume.all' if no targets were specified. @@ -207,7 +208,7 @@ Implementation of the 'generate' verb for HackMyResume. var to = PATH.resolve(t), pa = parsePath(to),fmat = pa.extname || '.all'; - var explicitFormats = _.omit( theTheme.formats, function(val) { + var explicitFormats = _.omit( theTheme.formats, function(val, key) { return !val.freebie; }); var implicitFormats = _.omit( theTheme.formats, function(val) { From df27924ac20d254307aed919408fbafde4397f74 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Wed, 30 Dec 2015 21:07:28 -0500 Subject: [PATCH 39/39] Add Johnny Trouble to tests. --- package.json | 3 +- test/test-fresh-sheet.js | 89 +++++++++++++++++++--------------------- test/test-themes.js | 47 ++++++++++----------- 3 files changed, 65 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index 79c689c..bef8d59 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "colors": "^1.1.2", "copy": "^0.1.3", - "fresh-themes": "~0.9.2-beta", + "fresh-themes": "~0.9.3-beta", "fresca": "~0.2.2", "fs-extra": "^0.24.0", "handlebars": "^4.0.5", @@ -79,6 +79,7 @@ "grunt-contrib-yuidoc": "^0.10.0", "grunt-simple-mocha": "*", "jane-q-fullstacker": "fluentdesk/jane-q-fullstacker", + "johnny-trouble-resume": "fluentdesk/johnny-trouble-resume", "jsonresume-theme-boilerplate": "^0.1.2", "jsonresume-theme-classy": "^1.0.9", "jsonresume-theme-modern": "0.0.18", diff --git a/test/test-fresh-sheet.js b/test/test-fresh-sheet.js index 4d28559..eb2ec7c 100644 --- a/test/test-fresh-sheet.js +++ b/test/test-fresh-sheet.js @@ -9,59 +9,54 @@ var chai = require('chai') chai.config.includeStack = false; -describe('jane-doe.json (FRESH)', function () { +function testResume(opts) { - var _sheet; + describe( opts.title + ' (FRESH)', function () { - it('should open without throwing an exception', function () { - function tryOpen() { - _sheet = new FRESHResume().open( - 'node_modules/jane-q-fullstacker/resume/jane-resume.json' ); - } - tryOpen.should.not.Throw(); - }); + var _sheet; - it('should have one or more of each section', function() { - expect( - //(_sheet.basics) && - _sheet.name && _sheet.info && _sheet.location && _sheet.contact && - (_sheet.employment.history && _sheet.employment.history.length > 0) && - (_sheet.skills && _sheet.skills.list.length > 0) && - (_sheet.education.history && _sheet.education.history.length > 0) && - (_sheet.service.history && _sheet.service.history.length > 0) && - (_sheet.writing && _sheet.writing.length > 0) && - (_sheet.recognition && _sheet.recognition.length > 0) && - (_sheet.samples && _sheet.samples.length > 0) && - (_sheet.references && _sheet.references.length > 0) && - (_sheet.interests && _sheet.interests.length > 0) - ).to.equal( true ); - }); + it('should open without throwing an exception', function () { + function tryOpen() { + _sheet = new FRESHResume().open( opts.path ); + } + tryOpen.should.not.Throw(); + }); - it('should have a work duration of 7 years', function() { - _sheet.computed.numYears.should.equal( 7 ); - }); + it('should have one or more of each section', function() { + var newObj = _.pick( _sheet, opts.sections ); + expect( Object.keys(newObj).length ).to.equal( opts.sections.length ); + }); - it('should save without throwing an exception', function(){ - function trySave() { - _sheet.save( 'test/sandbox/jane-q-fullstacker.json' ); - } - trySave.should.not.Throw(); - }); + it('should have a work duration of ' + opts.duration + ' years', function() { + _sheet.computed.numYears.should.equal( opts.duration ); + }); - it('should not be modified after saving', function() { - var savedSheet = new FRESHResume().open('test/sandbox/jane-q-fullstacker.json'); - _sheet.stringify().should.equal( savedSheet.stringify() ); - }); + it('should save without throwing an exception', function(){ + function trySave() { + _sheet.save( 'test/sandbox/' + opts.title + '.json' ); + } + trySave.should.not.Throw(); + }); - it('should validate against the FRESH resume schema', function() { - var result = _sheet.isValid(); - // var schemaJson = require('fresca'); - // var validate = validator( schemaJson, { verbose: true } ); - // var result = validate( JSON.parse( _sheet.imp.raw ) ); - result || console.log("\n\nOops, resume didn't validate. " + - "Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n"); - result.should.equal( true ); - }); + it('should not be modified after saving', function() { + var savedSheet = new FRESHResume().open('test/sandbox/' + opts.title + '.json'); + _sheet.stringify().should.equal( savedSheet.stringify() ); + }); + + it('should validate against the FRESH resume schema', function() { + var result = _sheet.isValid(); + // var schemaJson = require('fresca'); + // var validate = validator( schemaJson, { verbose: true } ); + // var result = validate( JSON.parse( _sheet.imp.raw ) ); + result || console.log("\n\nOops, resume didn't validate. " + + "Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n"); + result.should.equal( true ); + }); -}); + }); +} + +var sects = [ 'info', 'employment', 'service', 'skills', 'education', 'writing', 'recognition', 'references' ]; +testResume({ title: 'jane-q-fullstacker', path: 'node_modules/jane-q-fullstacker/resume/jane-resume.json', duration: 7, sections: sects }); +testResume({ title: 'johnny-trouble-resume', path: 'node_modules/johnny-trouble-resume/src/johnny-trouble.fresh.json', duration: 3, sections: sects }); diff --git a/test/test-themes.js b/test/test-themes.js index 5a6ee1d..ba9496a 100644 --- a/test/test-themes.js +++ b/test/test-themes.js @@ -12,7 +12,9 @@ var SPAWNWATCHER = require('../src/core/spawn-watch') chai.config.includeStack = false; -describe('Testing themes', function () { +function genThemes( title, src, fmt ) { + + describe('Testing themes against ' + title.toUpperCase() + ' resume ' + '(' + fmt + ')' , function () { var _sheet; @@ -29,11 +31,11 @@ describe('Testing themes', function () { function genTheme( fmt, src, themeName, themeLoc, testTitle ) { themeLoc = themeLoc || themeName; - testTitle = themeName.toUpperCase() + ' theme should generate without throwing an exception'; + testTitle = themeName.toUpperCase() + ' theme (' + fmt + ') should generate without throwing an exception'; it( testTitle, function () { function tryOpen() { //var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; - var dst = ['test/sandbox/' + fmt + '/' + themeName + '/resume.all']; + var dst = ['test/sandbox/' + fmt + '/' + title + '/' + themeName + '/resume.all']; var opts = { theme: themeLoc, format: fmt, @@ -46,28 +48,21 @@ describe('Testing themes', function () { }); } - 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' ); + genTheme(fmt, src, 'hello-world'); + genTheme(fmt, src, 'compact'); + genTheme(fmt, src, 'modern'); + genTheme(fmt, src, 'minimist'); + genTheme(fmt, src, 'awesome'); + genTheme(fmt, src, 'positive'); + genTheme(fmt, src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' ); + genTheme(fmt, src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' ); + genTheme(fmt, src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' ); + genTheme(fmt, 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' ); + }); -}); +} + +genThemes( 'jane-q-fullstacker', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], 'FRESH' ); +genThemes( 'johnny-trouble', ['node_modules/johnny-trouble-resume/src/johnny-trouble.fresh.json'], 'FRESH' ); +genThemes( 'richard-hendriks', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], 'JRS' );