Merge pull request #25 from fluentdesk/v0.11.0

v0.11.0
This commit is contained in:
hacksalot 2015-12-19 08:58:59 -05:00
commit 786b3fd3b2
14 changed files with 276 additions and 140 deletions

View File

@ -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' ]);

View File

@ -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&mdash;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

BIN
assets/resume-bouqet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -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 <hacksalot@fluentdesk.com> (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"
}

View File

@ -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;

View File

@ -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>|<\/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);
});
}
}());

View File

@ -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>|<\/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() ?
('<a href="' + url + '">' + text + '</a>') : 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);
});
};
}());

View File

@ -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;
};
}());

View File

@ -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;
}

View File

@ -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
}
]
}
}

View File

@ -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' );

View File

@ -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() )
});

View File

@ -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() )
});

View File

@ -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');
});