diff --git a/Gruntfile.js b/Gruntfile.js index fd9987b..ee65f40 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -17,6 +17,8 @@ module.exports = function (grunt) { all: { src: ['tests/*.js'] } }, + clean: ['tests/sandbox'], + yuidoc: { compile: { name: '<%= pkg.name %>', @@ -46,9 +48,10 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-simple-mocha'); grunt.loadNpmTasks('grunt-contrib-yuidoc'); grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-clean'); grunt.registerTask('test', 'Test the FluentCV library.', - function( config ) { grunt.task.run( ['simplemocha:all'] ); }); + function( config ) { grunt.task.run( ['clean','simplemocha:all'] ); }); grunt.registerTask('document', 'Generate FluentCV library documentation.', function( config ) { grunt.task.run( ['yuidoc'] ); }); grunt.registerTask('default', [ 'jshint', 'test', 'yuidoc' ]); diff --git a/README.md b/README.md index 23d7203..6d0273d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ fluentCV ======== -*Create polished technical résumés and CVs in multiple formats from your command -line or shell. See [FluentCV Desktop][7] for the desktop version. OS X ~ Windows -~ Linux.* +*Create polished résumés and CVs in multiple formats from your command line or +shell. Author in clean Markdown and JSON, export to Word, HTML, PDF, LaTeX, +plain text, and other arbitrary formats.* -![](assets/fluentcv_cli_ubuntu.png) +![](assets/resume-bouqet.png) -FluentCV is a dev-friendly Swiss Army knife for resumes and CVs. Use it to: +FluentCV is a dev-friendly, local-only Swiss Army knife for resumes and CVs. Use +it to: 1. **Generate** HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, print, smoke signal, carrier pigeon, and other arbitrary-format resumes @@ -14,7 +15,10 @@ and CVs, from a single source of truth—without violating DRY. 2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats. 3. **Validate** resumes against either format. -FluentCV supports both the [FRESH][fresca] and [JSON Resume][6] source formats. +FluentCV is built with Node.js and runs on recent versions of OS X, Linux, or +Windows. + +![](assets/fluentcv_cli_ubuntu.png) ## Features diff --git a/assets/resume-bouqet.png b/assets/resume-bouqet.png new file mode 100644 index 0000000..a8acf08 Binary files /dev/null and b/assets/resume-bouqet.png differ diff --git a/package.json b/package.json index fb0cfbd..ffea534 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fluentcv", - "version": "0.10.3", + "version": "0.11.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", @@ -20,7 +20,7 @@ "HTML", "CLI" ], - "author": "James M. Devlin", + "author": "hacksalot (https://github.com/hacksalot)", "license": "MIT", "preferGlobal": "true", "bugs": { @@ -33,8 +33,8 @@ "homepage": "https://github.com/fluentdesk/fluentcv", "dependencies": { "colors": "^1.1.2", - "fluent-themes": "~0.6.3-beta", - "fresca": "~0.2.1", + "fluent-themes": "~0.7.0-beta", + "fresca": "~0.2.2", "fs-extra": "^0.24.0", "handlebars": "^4.0.5", "html": "0.0.10", @@ -54,10 +54,11 @@ "devDependencies": { "chai": "*", "grunt": "*", + "grunt-contrib-clean": "^0.7.0", "grunt-contrib-jshint": "^0.11.3", "grunt-contrib-yuidoc": "^0.10.0", "grunt-simple-mocha": "*", - "is-my-json-valid": "^2.12.2", + "jane-q-fullstacker": "fluentdesk/jane-q-fullstacker", "mocha": "*", "resample": "fluentdesk/resample" } diff --git a/src/core/theme.js b/src/core/theme.js index 01a52f7..2a71a2e 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -100,6 +100,7 @@ Definition of the Theme class. var outFmt = '', isMajor = false; var portion = pathInfo.dir.replace(tplFolder,''); if( portion && portion.trim() ) { + if( portion[1] === '_' ) return; var reg = /^(?:\/|\\)(html|latex|doc|pdf|partials)(?:\/|\\)?/ig; var res = reg.exec( portion ); if( res ) { @@ -152,7 +153,7 @@ Definition of the Theme class. .forEach(function( cssf ) { // For each CSS file, get its corresponding HTML file var idx = _.findIndex(fmts, function( fmt ) { - return fmt.pre === cssf.pre && fmt.ext === 'html'; + return fmt && fmt.pre === cssf.pre && fmt.ext === 'html'; }); cssf.action = null; fmts[ idx ].css = cssf.data; diff --git a/src/eng/handlebars-generator.js b/src/eng/handlebars-generator.js index 33dbc12..8993811 100644 --- a/src/eng/handlebars-generator.js +++ b/src/eng/handlebars-generator.js @@ -11,93 +11,40 @@ Definition of the HandlebarsGenerator class. var _ = require('underscore') , HANDLEBARS = require('handlebars') , FS = require('fs') - , moment = require('moment') - , MD = require('marked') - , H2W = require('../utils/html-to-wpml'); + , registerHelpers = require('./handlebars-helpers'); /** Perform template-based resume generation using Handlebars.js. - @method generate + @class HandlebarsGenerator */ - module.exports = function( json, jst, format, cssInfo, opts, theme ) { + var HandlebarsGenerator = module.exports = { - // Pre-compile any partials present in the theme. - _.each( theme.partials, function( el ) { - var tplData = FS.readFileSync( el.path, 'utf8' ); - var compiledTemplate = HANDLEBARS.compile( tplData ); - HANDLEBARS.registerPartial( el.name, compiledTemplate ); - }); + generate: function( json, jst, format, cssInfo, opts, theme ) { - // Register necessary helpers. - registerHelpers(); + // Pre-compile any partials present in the theme. + _.each( theme.partials, function( el ) { + var tplData = FS.readFileSync( el.path, 'utf8' ); + var compiledTemplate = HANDLEBARS.compile( tplData ); + HANDLEBARS.registerPartial( el.name, compiledTemplate ); + }); - // Compile and run the Handlebars template. - var template = HANDLEBARS.compile(jst); - return template({ - r: json, - filt: opts.filters, - cssInfo: cssInfo, - headFragment: opts.headFragment || '' - }); + // Register necessary helpers. + registerHelpers(); + + // Compile and run the Handlebars template. + var template = HANDLEBARS.compile(jst); + return template({ + r: format === 'html' || format === 'pdf' ? json.markdownify() : json, + RAW: json, + filt: opts.filters, + cssInfo: cssInfo, + headFragment: opts.headFragment || '' + }); + + } }; - - - /** - Register useful Handlebars helpers. - @method registerHelpers - */ - function registerHelpers() { - - // Set up a date formatting helper so we can do: - // {{#formatDate val 'YYYY-MM'}} - HANDLEBARS.registerHelper("formatDate", function(datetime, format) { - if( moment ) { - return moment( datetime ).format( format ); - } - else { - return datetime; - } - }); - - // Set up a Markdown-to-WordProcessingML helper so we can do: - // {{#wmpl val [true|false]}} - HANDLEBARS.registerHelper("wpml", function( txt, inline ) { - inline = (inline && !inline.hash) || false; - txt = inline ? - MD(txt.trim()).replace(/^\s*

|<\/p>\s*$/gi, '') : - MD(txt.trim()); - txt = H2W( txt.trim() ); - return txt; - }); - - // Set up a generic conditional helper so we can do: - // {{#compare val otherVal operator="<"}} - // http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates/ - HANDLEBARS.registerHelper('compare', function(lvalue, rvalue, options) { - if (arguments.length < 3) - throw new Error("Handlerbars Helper 'compare' needs 2 parameters"); - var operator = options.hash.operator || "=="; - var operators = { - '==': function(l,r) { return l == r; }, - '===': function(l,r) { return l === r; }, - '!=': function(l,r) { return l != r; }, - '<': function(l,r) { return l < r; }, - '>': function(l,r) { return l > r; }, - '<=': function(l,r) { return l <= r; }, - '>=': function(l,r) { return l >= r; }, - 'typeof': function(l,r) { return typeof l == r; } - }; - if (!operators[operator]) - throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator); - var result = operators[operator](lvalue,rvalue); - return result ? options.fn(this) : options.inverse(this); - }); - } - - - }()); diff --git a/src/eng/handlebars-helpers.js b/src/eng/handlebars-helpers.js new file mode 100644 index 0000000..e3458bf --- /dev/null +++ b/src/eng/handlebars-helpers.js @@ -0,0 +1,124 @@ +/** +Template helper definitions for Handlebars. +@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@module handlebars-helpers.js +*/ + + +(function() { + + var HANDLEBARS = require('handlebars') + , MD = require('marked') + , H2W = require('../utils/html-to-wpml') + , moment = require('moment') + , _ = require('underscore'); + + /** + Register useful Handlebars helpers. + @method registerHelpers + */ + module.exports = function() { + + // Set up a date formatting helper so we can do: + // {{formatDate val 'YYYY-MM'}} + HANDLEBARS.registerHelper("formatDate", function(datetime, format) { + return moment ? moment( datetime ).format( format ) : datetime; + }); + + // Set up a Markdown-to-WordProcessingML helper so we can do: + // {{wmpl val [true|false]}} + HANDLEBARS.registerHelper("wpml", function( txt, inline ) { + if(!txt) return ''; + inline = (inline && !inline.hash) || false; + txt = inline ? + MD(txt.trim()).replace(/^\s*

|<\/p>\s*$/gi, '') : + MD(txt.trim()); + txt = H2W( txt.trim() ); + return txt; + }); + + // Set up a last-word helper so we can do: + // {{lastWord val [true|false]}} + HANDLEBARS.registerHelper("link", function( text, url ) { + return url && url.trim() ? + ('' + text + '') : text; + }); + + // Set up a last-word helper so we can do: + // {{lastWord val [true|false]}} + HANDLEBARS.registerHelper("lastWord", function( txt ) { + return txt && txt.trim() ? _.last( txt.split(' ') ) : ''; + }); + + // Set up a skill colorizing helper: + // {{skillColor val}} + HANDLEBARS.registerHelper("skillColor", function( lvl ) { + switch(lvl) { + case 'beginner': return '#5CB85C'; + case 'intermediate': return '#F1C40F'; + case 'advanced': return '#428BCA'; + case 'master': return '#C00000'; + } + }); + + // Set up a skill colorizing helper: + // {{skillColor val}} + HANDLEBARS.registerHelper("skillHeight", function( lvl ) { + switch(lvl) { + case 'beginner': return '30'; + case 'intermediate': return '16'; + case 'advanced': return '8'; + case 'master': return '0'; + } + }); + + // Set up a Markdown-to-WordProcessingML helper so we can do: + // {{initialWords val [true|false]}} + HANDLEBARS.registerHelper("initialWords", function( txt ) { + return txt && txt.trim() ? _.initial( txt.split(' ') ).join(' ') : ''; + }); + + // Set up a URL-trimming helper to drop the protocol so we can do: + // {{trimURL url}} + HANDLEBARS.registerHelper("trimURL", function( url ) { + return url && url.trim() ? url.trim().replace(/^https?:\/\//i, '') : ''; + }); + + // Set up a URL-trimming helper to drop the protocol so we can do: + // {{trimURL url}} + HANDLEBARS.registerHelper("toLower", function( txt ) { + return txt && txt.trim() ? txt.toLowerCase() : ''; + }); + + // Set up a Markdown-to-WordProcessingML helper so we can do: + // {{either A B}} + HANDLEBARS.registerHelper("either", function( lhs, rhs, options ) { + if (lhs || rhs) return options.fn(this); + }); + + // Set up a generic conditional helper so we can do: + // {{compare val otherVal operator="<"}} + // http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates/ + HANDLEBARS.registerHelper('compare', function(lvalue, rvalue, options) { + if (arguments.length < 3) + throw new Error("Handlerbars Helper 'compare' needs 2 parameters"); + var operator = options.hash.operator || "=="; + var operators = { + '==': function(l,r) { return l == r; }, + '===': function(l,r) { return l === r; }, + '!=': function(l,r) { return l != r; }, + '<': function(l,r) { return l < r; }, + '>': function(l,r) { return l > r; }, + '<=': function(l,r) { return l <= r; }, + '>=': function(l,r) { return l >= r; }, + 'typeof': function(l,r) { return typeof l == r; } + }; + if (!operators[operator]) + throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator); + var result = operators[operator](lvalue,rvalue); + return result ? options.fn(this) : options.inverse(this); + }); + + }; + +}()); diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js index 755155d..fe2fb36 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -1,37 +1,52 @@ /** Definition of the UnderscoreGenerator class. @license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@module underscore-generator.js */ (function() { + + var _ = require('underscore'); - module.exports = function( json, jst, format, cssInfo, opts, theme ) { - // Tweak underscore's default template delimeters - var delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template; - if( opts.themeObj && opts.themeObj.delimeters ) { - delims = _.mapObject( delims, function(val,key) { - return new RegExp( val, "ig"); + + /** + Perform template-based resume generation using Underscore.js. + @class UnderscoreGenerator + */ + var UnderscoreGenerator = module.exports = { + + generate: function( json, jst, format, cssInfo, opts, theme ) { + + // Tweak underscore's default template delimeters + var delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template; + if( opts.themeObj && opts.themeObj.delimeters ) { + delims = _.mapObject( delims, function(val,key) { + return new RegExp( val, "ig"); + }); + } + _.templateSettings = delims; + + // Strip {# comments #} + jst = jst.replace( delims.comment, ''); + + // Compile and run the template. TODO: avoid unnecessary recompiles. + var compiled = _.template(jst); + var ret = compiled({ + r: format === 'html' || format === 'pdf' ? json.markdownify() : json, + filt: opts.filters, + XML: require('xml-escape'), + RAW: json, + cssInfo: cssInfo, + headFragment: opts.headFragment || '' }); + return ret; } - _.templateSettings = delims; - - // Strip {# comments #} - jst = jst.replace( delims.comment, ''); - // Compile and run the template. TODO: avoid unnecessary recompiles. - var compiled = _.template(jst); - var ret = compiled({ - r: format === 'html' || format === 'pdf' ? json.markdownify() : json, - filt: opts.filters, - XML: require('xml-escape'), - RAW: json, - cssInfo: cssInfo, - headFragment: opts.headFragment || '' - }); - return ret; }; + + }()); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 5645022..fb6ee39 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -142,9 +142,8 @@ Definition of the TemplateGenerator class. */ single: function( json, jst, format, cssInfo, opts, theme ) { this.opts.freezeBreaks && ( jst = freeze(jst) ); - var eng = require( '../eng/' + ((opts.themeObj && opts.themeObj.engine) || - opts.engine) + '-generator' ); - var result = eng( json, jst, format, cssInfo, opts, theme ); + var eng = require( '../eng/' + theme.engine + '-generator' ); + var result = eng.generate( json, jst, format, cssInfo, opts, theme ); this.opts.freezeBreaks && ( result = unfreeze(result) ); return result; } diff --git a/tests/resumes/jrs/jane-doe.json b/tests/resumes/jrs/jane-q-fullstacker.json similarity index 82% rename from tests/resumes/jrs/jane-doe.json rename to tests/resumes/jrs/jane-q-fullstacker.json index 3f98b1f..476aadf 100644 --- a/tests/resumes/jrs/jane-doe.json +++ b/tests/resumes/jrs/jane-q-fullstacker.json @@ -1,14 +1,14 @@ { "basics": { - "name": "Jane Doe", - "label": "Senior Developer / Code Ninja", + "name": "Jane Q. Fullstacker", + "label": "Senior Developer", "summary": "**Full-stack software developer with 6+ years industry experience** specializing in scalable cloud architectures for this, that, and the other. A native of southern CA, Jane enjoys hiking, mystery novels, and the company of Rufus, her two-year-old beagle.", - "website": "http://jane-doe.me", + "website": "http://janef.me/blog", "phone": "1-650-999-7777", "email": "jdoe@onecoolstartup.io", "picture": "jane_doe.png", "location": { - "address": "Jane Doe\n123 Somewhere Rd.\nMountain View, CA 94035", + "address": "Jane Fullstacker\n123 Somewhere Rd.\nMountain View, CA 94035", "postalCode": "94035", "city": "Mountain View", "countryCode": "US", @@ -17,13 +17,13 @@ "profiles": [ { "network": "GitHub", - "username": "jane-doe-was-here", - "url": "https://github.com/jane-doe-was-here" + "username": "janef-was-here", + "url": "https://github.com/janef-was-here" }, { "network": "Twitter", - "username": "jane-doe-was-here", - "url": "https://twitter.com/jane-doe-was-here" + "username": "janef-was-here", + "url": "https://twitter.com/janef-was-here" } ] }, @@ -104,17 +104,55 @@ ], "skills": [ { - "name": "Programming", + "name": "Web Dev", "keywords": [ - "C++", - "Ruby", - "Xcode" + "JavaScript", + "HTML 5", + "CSS", + "LAMP", + "MVC", + "REST" ] }, { - "name": "Project Management", + "name": "JavaScript", "keywords": [ - "Agile" + "Node.js", + "Angular.js", + "jQuery", + "Bootstrap", + "React.js", + "Backbone.js" + ] + }, + { + "name": "Database", + "keywords": [ + "MySQL", + "PostgreSQL", + "NoSQL", + "ORM", + "Hibernate" + ] + }, + { + "name": "Cloud", + "keywords": [ + "AWS", + "EC2", + "RDS", + "S3", + "Azure", + "Dropbox" + ] + }, + { + "name": "Project", + "keywords": [ + "Agile", + "TFS", + "Unified Process", + "MS Project" ] } ], @@ -166,10 +204,10 @@ "website": "http://codeproject.com/build-ui-electron-atom.aspx" }, { - "name": "Jane Doe Unplugged", + "name": "Jane Fullstacker's Blog", "publisher": "self", "releaseDate": "2011", - "website": "http://jane-doe.me" + "website": "http://janef.me" }, { "name": "Teach Yourself GORFF in 21 Days", @@ -218,7 +256,8 @@ }, { "language": "Spanish", - "level": "Moderate" + "level": "Moderate", + "years": 10 } ] -} +} \ No newline at end of file diff --git a/tests/test-converter.js b/tests/test-converter.js index 134f086..d66368e 100644 --- a/tests/test-converter.js +++ b/tests/test-converter.js @@ -7,6 +7,7 @@ var chai = require('chai') , FRESHResume = require('../src/core/fresh-resume') , CONVERTER = require('../src/core/convert') , FS = require('fs') + , MKDIRP = require('mkdirp') , _ = require('underscore'); chai.config.includeStack = false; @@ -21,6 +22,7 @@ describe('FRESH/JRS converter', function () { var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' ); _sheet = new FRESHResume().open( fileA ); + MKDIRP.sync( path.parse(fileB).dir ); _sheet.saveAs( fileB, 'JRS' ); var rawA = FS.readFileSync( fileA, 'utf8' ); diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js index 295f1ed..955c924 100644 --- a/tests/test-fresh-sheet.js +++ b/tests/test-fresh-sheet.js @@ -16,7 +16,7 @@ describe('jane-doe.json (FRESH)', function () { it('should open without throwing an exception', function () { function tryOpen() { _sheet = new FRESHResume().open( - 'node_modules/FRESCA/exemplar/jane-doe.json' ); + 'node_modules/jane-q-fullstacker/resume/jane-resume.json' ); } tryOpen.should.not.Throw(); }); @@ -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-doe.json' ); + _sheet.save( 'tests/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-doe.json'); + var savedSheet = new FRESHResume().open('tests/sandbox/jane-q-fullstacker.json'); _sheet.stringify().should.equal( savedSheet.stringify() ) }); diff --git a/tests/test-jrs-sheet.js b/tests/test-jrs-sheet.js index 815b3aa..ee4b1b1 100644 --- a/tests/test-jrs-sheet.js +++ b/tests/test-jrs-sheet.js @@ -16,7 +16,7 @@ describe('jane-doe.json (JRS)', function () { it('should open without throwing an exception', function () { function tryOpen() { _sheet = new JRSResume().open( - path.join( __dirname, 'resumes/jrs/jane-doe.json' ) ); + path.join( __dirname, 'resumes/jrs/jane-q-fullstacker.json' ) ); } tryOpen.should.not.Throw(); }); @@ -39,13 +39,13 @@ describe('jane-doe.json (JRS)', function () { it('should save without throwing an exception', function(){ function trySave() { - _sheet.save( 'tests/sandbox/jane-doe.json' ); + _sheet.save( 'tests/sandbox/jane-q-fullstacker.json' ); } trySave.should.not.Throw(); }); it('should not be modified after saving', function() { - var savedSheet = new JRSResume().open( 'tests/sandbox/jane-doe.json' ); + var savedSheet = new JRSResume().open( 'tests/sandbox/jane-q-fullstacker.json' ); _sheet.stringify().should.equal( savedSheet.stringify() ) }); diff --git a/tests/test-themes.js b/tests/test-themes.js index c224eda..0bb3ffe 100644 --- a/tests/test-themes.js +++ b/tests/test-themes.js @@ -29,8 +29,8 @@ describe('Testing themes', function () { function genTheme( themeName ) { it( themeName.toUpperCase() + ' theme should generate without throwing an exception', function () { function tryOpen() { - var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; - var dst = ['tests/sandbox/hello-world/resume.all']; + var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json']; + var dst = ['tests/sandbox/' + themeName + '/resume.all']; var opts = { theme: themeName, format: 'FRESH', @@ -48,5 +48,6 @@ describe('Testing themes', function () { genTheme('modern'); genTheme('minimist'); genTheme('awesome'); + genTheme('positive'); });