1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-11-22 16:30:11 +00:00

Merge pull request #20 from fluentdesk/v0.10.0

v0.10.0
This commit is contained in:
Sir Hacksalot 2015-12-09 23:28:25 -05:00
commit a3cefa1c42
21 changed files with 1321 additions and 553 deletions

View File

@ -1,7 +1,7 @@
'use strict';
module.exports = function (grunt) { module.exports = function (grunt) {
'use strict';
var opts = { var opts = {
pkg: grunt.file.readJSON('package.json'), pkg: grunt.file.readJSON('package.json'),
@ -29,19 +29,28 @@ module.exports = function (grunt) {
outdir: 'docs/' outdir: 'docs/'
} }
} }
},
jshint: {
options: {
laxcomma: true,
expr: true
},
all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js']
} }
}; };
grunt.initConfig( opts ); grunt.initConfig( opts );
grunt.loadNpmTasks('grunt-simple-mocha'); grunt.loadNpmTasks('grunt-simple-mocha');
grunt.loadNpmTasks('grunt-contrib-yuidoc'); grunt.loadNpmTasks('grunt-contrib-yuidoc');
grunt.registerTask('test', 'Test the FluentCV library.', function( config ) { grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.task.run( ['simplemocha:all'] );
}); grunt.registerTask('test', 'Test the FluentCV library.',
grunt.registerTask('document', 'Generate FluentCV library documentation.', function( config ) { function( config ) { grunt.task.run( ['simplemocha:all'] ); });
grunt.task.run( ['yuidoc'] ); grunt.registerTask('document', 'Generate FluentCV library documentation.',
}); function( config ) { grunt.task.run( ['yuidoc'] ); });
grunt.registerTask('default', [ 'test', 'yuidoc' ]); grunt.registerTask('default', [ 'jshint', 'test', 'yuidoc' ]);
}; };

View File

@ -1,7 +1,7 @@
The MIT License The MIT License
=============== ===============
Copyright (c) 2015 James M. Devlin (https://github.com/devlinjd) Copyright (c) 2015 James M. Devlin (https://github.com/hacksalot)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,47 +1,51 @@
fluentCV fluentCV
======== ========
*Generate beautiful, targeted resumes from your command line or shell.* *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.*
![](assets/fluentcv_cli_ubuntu.png) ![](assets/fluentcv_cli_ubuntu.png)
FluentCV is a Swiss Army knife for resumes and CVs. Use it to: FluentCV is a dev-friendly Swiss Army knife for resumes and CVs. Use it to:
1. **Generate** polished multiformat resumes in HTML, Word, Markdown, PDF, plain 1. **Generate** HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML,
text, JSON, and YAML formats—without violating DRY. YAML, print, smoke signal, carrier pigeon, and other arbitrary-format resumes
and CVs, from a single source of truth—without violating DRY.
2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats. 2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats.
3. **Validate** resumes against either format. 3. **Validate** resumes against either format.
Install it with NPM: FluentCV supports both the [FRESH][fresca] and [JSON Resume][6] source formats.
## Features
- OS X, Linux, and Windows.
- Store your resume data as a durable, versionable JSON or YAML document.
- Generate polished resumes in multiple formats without violating [DRY][dry].
- Output to HTML, Markdown, LaTeX, PDF, MS Word, JSON, YAML, plain text, or XML.
- Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and
[FCV Desktop][7].
- Validate resumes against the FRESH or JSON Resume schema.
- Support for multiple input and output resumes.
- Use from your command line or [desktop][7].
- Free and open-source through the MIT license.
## Install
Install FluentCV with NPM:
```bash ```bash
[sudo] npm install fluentcv -g [sudo] npm install fluentcv -g
``` ```
Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for
your platform. your platform. For LaTeX generation you'll need a valid LaTeX environment with
access to `xelatex` and similar.
## Features
- Runs on OS X, Linux, and Windows.
- Store your resume data as a durable, versionable JSON or YAML document.
- Generate polished resumes in multiple formats without violating [DRY][dry].
- Output to HTML, PDF, Markdown, MS Word, JSON, YAML, plain text, or XML.
- Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and
[FCV Desktop][7].
- Validate resumes against the FRESH or JSON Resume schema.
- Support for multiple input and output resumes.
- Free and open-source through the MIT license.
- Forthcoming: StackOverflow and LinkedIn support.
- Forthcoming: More commands and themes.
Looking for a desktop GUI version for Windows / OS X / Linux? Check out
[FluentCV Desktop][7].
## Getting Started ## Getting Started
To use FluentCV you'll need to create a valid resume in either [FRESH][fresca] To use FluentCV you'll need to create a valid resume in either [FRESH][fresca]
or [JSON Resume][6] format. Then you can start using the command line tool. or [JSON Resume][6] format. Then you can start using the command line tool.
There are three commands you should be aware of: There are four basic commands you should be aware of:
- `build` generates resumes in HTML, Word, Markdown, PDF, and other formats. Use - `build` generates resumes in HTML, Word, Markdown, PDF, and other formats. Use
it when you need to submit, upload, print, or email resumes in specific formats. it when you need to submit, upload, print, or email resumes in specific formats.
@ -52,6 +56,15 @@ it when you need to submit, upload, print, or email resumes in specific formats.
fluentcv BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all fluentcv BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all
``` ```
- `new` creates a new resume in FRESH or JSON Resume format.
```bash
# fluentcv NEW <OUTPUTS> [-f <FORMAT>]
fluentcv NEW resume.json
fluentcv NEW resume.json -f fresh
fluentcv NEW r1.json r2.json -f jrs
```
- `convert` converts your source resume between FRESH and JSON Resume formats. - `convert` converts your source resume between FRESH and JSON Resume formats.
Use it to convert between the two formats to take advantage of tools and services. Use it to convert between the two formats to take advantage of tools and services.
@ -78,6 +91,7 @@ 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. 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. 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. MS Word | .doc | A Microsoft Word office document.
Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme. Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme.
plain text | .txt | A formatted plain text document appropriate for emails or copy-paste. plain text | .txt | A formatted plain text document appropriate for emails or copy-paste.
@ -92,6 +106,7 @@ image | .png, .bmp | Forthcoming.
FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then: FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then:
1. Install the latest official [wkhtmltopdf][3] binary for your platform. 1. Install the latest official [wkhtmltopdf][3] binary for your platform.
2. Optionally install an updated LaTeX environment (LaTeX resumes only).
2. Install **fluentCV** with `[sudo] npm install fluentcv -g`. 2. Install **fluentCV** with `[sudo] npm install fluentcv -g`.
3. You're ready to go. 3. You're ready to go.

View File

@ -1,6 +1,6 @@
{ {
"name": "fluentcv", "name": "fluentcv",
"version": "0.9.1", "version": "0.10.0",
"description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.", "description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.",
"repository": { "repository": {
"type": "git", "type": "git",
@ -32,10 +32,11 @@
}, },
"homepage": "https://github.com/fluentdesk/fluentcv", "homepage": "https://github.com/fluentdesk/fluentcv",
"dependencies": { "dependencies": {
"FRESCA": "fluentdesk/FRESCA#v0.1.0", "fresca": "^0.2.0",
"colors": "^1.1.2", "colors": "^1.1.2",
"fluent-themes": "0.5.1-beta", "fluent-themes": "0.6.0-beta",
"fs-extra": "^0.24.0", "fs-extra": "^0.24.0",
"handlebars": "^4.0.5",
"html": "0.0.10", "html": "0.0.10",
"is-my-json-valid": "^2.12.2", "is-my-json-valid": "^2.12.2",
"jst": "0.0.13", "jst": "0.0.13",
@ -43,6 +44,7 @@
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"moment": "^2.10.6", "moment": "^2.10.6",
"recursive-readdir-sync": "^1.0.6",
"underscore": "^1.8.3", "underscore": "^1.8.3",
"wkhtmltopdf": "^0.1.5", "wkhtmltopdf": "^0.1.5",
"xml-escape": "^1.0.0", "xml-escape": "^1.0.0",
@ -51,6 +53,7 @@
"devDependencies": { "devDependencies": {
"chai": "*", "chai": "*",
"grunt": "*", "grunt": "*",
"grunt-contrib-jshint": "^0.11.3",
"grunt-contrib-yuidoc": "^0.10.0", "grunt-contrib-yuidoc": "^0.10.0",
"grunt-simple-mocha": "*", "grunt-simple-mocha": "*",
"is-my-json-valid": "^2.12.2", "is-my-json-valid": "^2.12.2",

182
src/core/empty-fresh.json Normal file
View File

@ -0,0 +1,182 @@
{
"name": "",
"meta": {
"format": "FRESH@0.1.0",
"version": "0.1.0"
},
"info": {
"label": "",
"characterClass": "",
"brief": "",
"image": ""
},
"contact": {
"website": "",
"phone": "",
"email": "",
"other": []
},
"location": {
"address": "",
"city": "",
"region": "",
"code": "",
"country": ""
},
"social": [
{
"label": "",
"network": "",
"user": "",
"url": ""
}
],
"employment": {
"summary": "",
"history": [
{
"employer": "",
"url": "",
"position": "",
"summary": "",
"start": "",
"keywords": [],
"highlights": []
}
]
},
"education": {
"summary": "",
"level": "",
"degree": "",
"history": [
{
"institution": "",
"url": "",
"start": "",
"end": "",
"grade": "",
"summary": "",
"curriculum": []
}
]
},
"service": {
"summary": "",
"history": [
{
"flavor": "",
"position": "",
"organization": "",
"url": "",
"start": "",
"end": "",
"summary": "",
"highlights": []
}
]
},
"skills": {
"sets": [
{
"name": "",
"skills": []
}
],
"list": [ ]
},
"samples": [
{
"title": "",
"summary": "",
"url": "",
"date": ""
}
],
"writing": [
{
"title": "",
"flavor": "",
"date": "",
"publisher": {
"name": "",
"url": ""
},
"url": ""
}
],
"reading": [
{
"title": "",
"flavor": "",
"url": "",
"author": ""
}
],
"recognition": [
{
"flavor": "",
"from": "",
"title": "",
"event": "",
"date": "",
"summary": ""
}
],
"references": [
{
"name": "",
"flavor": "",
"private": true,
"contact": [
{
"label": "",
"flavor": "",
"value": ""
}
]
}
],
"testimonials": [
{
"name": "",
"flavor": "",
"quote": ""
}
],
"languages": [
{
"language": "",
"level": "",
"years": 0
}
],
"interests": [
{
"name": "",
"summary": "",
"keywords": []
}
]
}

View File

@ -34,8 +34,8 @@ FluentDate/*.prototype*/.fmt = function( dt ) {
else if( /^\D+\s+\d{4}$/.test(dt) ) { // "Mar 2015" else if( /^\D+\s+\d{4}$/.test(dt) ) { // "Mar 2015"
var parts = dt.split(' '); var parts = dt.split(' ');
var month = (months[parts[0]] || abbr[parts[0]]); var month = (months[parts[0]] || abbr[parts[0]]);
var dt = parts[1] + '-' + (month < 10 ? '0' + month : month.toString()); var temp = parts[1] + '-' + (month < 10 ? '0' + month : month.toString());
return moment( dt, 'YYYY-MM' ); return moment( temp, 'YYYY-MM' );
} }
else if( /^\d{4}-\d{1,2}$/.test(dt) ) { // "2015-03", "1998-4" else if( /^\d{4}-\d{1,2}$/.test(dt) ) { // "2015-03", "1998-4"
return moment( dt, 'YYYY-MM' ); return moment( dt, 'YYYY-MM' );

View File

@ -11,6 +11,7 @@ Definition of the FRESHResume class.
, _ = require('underscore') , _ = require('underscore')
, PATH = require('path') , PATH = require('path')
, moment = require('moment') , moment = require('moment')
, MD = require('marked')
, CONVERTER = require('./convert'); , CONVERTER = require('./convert');
/** /**
@ -54,7 +55,13 @@ Definition of the FRESHResume class.
FS.writeFileSync( this.imp.fileName, FreshResume.stringify( newRep ), 'utf8' ); FS.writeFileSync( this.imp.fileName, FreshResume.stringify( newRep ), 'utf8' );
} }
return this; return this;
} };
FreshResume.prototype.dupe = function() {
var rnew = new FreshResume();
rnew.parse( this.stringify(), { } );
return rnew;
};
/** /**
Convert the supplied object to a JSON string, sanitizing meta-properties along Convert the supplied object to a JSON string, sanitizing meta-properties along
@ -68,7 +75,60 @@ Definition of the FRESHResume class.
) ? undefined : value; ) ? undefined : value;
} }
return JSON.stringify( obj, replacer, 2 ); return JSON.stringify( obj, replacer, 2 );
}, };
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
FreshResume.prototype.markdownify = function() {
var that = this;
var ret = this.dupe();
function MDIN(txt){
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
// TODO: refactor recursion
function markdownifyStringsInObject( 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 ? MDIN(elem) : MD( elem );
else
markdownifyStringsInObject( 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','start','end','date'], key) )
return;
if( key === 'summary' )
obj[key] = MD( obj[key] );
else
obj[key] = inline ? MDIN( obj[key] ) : MD( obj[key] );
}
else
markdownifyStringsInObject( sub );
});
}
}
Object.keys( ret ).forEach(function(member){
markdownifyStringsInObject( ret[ member ] );
});
return ret;
};
/** /**
Convert this object to a JSON string, sanitizing meta-properties along the Convert this object to a JSON string, sanitizing meta-properties along the
@ -129,7 +189,7 @@ Definition of the FRESHResume class.
*/ */
FreshResume.prototype.updateData = function( str ) { FreshResume.prototype.updateData = function( str ) {
this.clear( false ); this.clear( false );
this.parse( str ) this.parse( str );
return this; return this;
}; };
@ -143,9 +203,10 @@ Definition of the FRESHResume class.
delete this.employment; delete this.employment;
delete this.service; delete this.service;
delete this.education; delete this.education;
//delete this.awards; delete this.recognition;
delete this.publications; delete this.reading;
//delete this.interests; delete this.writing;
delete this.interests;
delete this.skills; delete this.skills;
delete this.social; delete this.social;
}; };
@ -154,8 +215,9 @@ Definition of the FRESHResume class.
Get the default (empty) sheet. Get the default (empty) sheet.
*/ */
FreshResume.default = function() { FreshResume.default = function() {
return new FreshResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); return new FreshResume().open(
} PATH.join( __dirname, 'empty-fresh.json'), 'Empty' );
};
/** /**
Add work experience to the sheet. Add work experience to the sheet.
@ -178,6 +240,27 @@ Definition of the FRESHResume class.
}); });
}; };
/**
Return the specified network profile.
*/
FreshResume.prototype.getProfile = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.find( this.social, function(sn) {
return sn.network.trim().toLowerCase() === socialNetwork;
});
};
/**
Return an array of profiles for the specified network, for when the user
has multiple eg. GitHub accounts.
*/
FreshResume.prototype.getProfiles = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.filter( this.social, function(sn){
return sn.network.trim().toLowerCase() === socialNetwork;
});
};
/** /**
Determine if the sheet includes a specific skill. Determine if the sheet includes a specific skill.
*/ */
@ -195,7 +278,7 @@ Definition of the FRESHResume class.
*/ */
FreshResume.prototype.isValid = function( info ) { FreshResume.prototype.isValid = function( info ) {
var schemaObj = require('FRESCA'); var schemaObj = require('FRESCA');
var validator = require('is-my-json-valid') var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // Note [1] var validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
}); });
@ -243,7 +326,7 @@ Definition of the FRESHResume class.
// return( a.safeDate.isBefore(b.safeDate) ) ? 1 // return( a.safeDate.isBefore(b.safeDate) ) ? 1
// : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0; // : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
// }); // });
this.publications && this.publications.sort( function(a, b) { this.writing && this.writing.sort( function(a, b) {
return( a.safe.date.isBefore(b.safe.date) ) ? 1 return( a.safe.date.isBefore(b.safe.date) ) ? 1
: ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0; : ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0;
}); });
@ -265,35 +348,39 @@ Definition of the FRESHResume class.
function _parseDates() { function _parseDates() {
var _fmt = require('./fluent-date').fmt; var _fmt = require('./fluent-date').fmt;
var that = this;
this.employment.history && this.employment.history.forEach( function(job) { // TODO: refactor recursion
job.safe = { function replaceDatesInObject( obj ) {
start: _fmt( job.start ),
end: _fmt( job.end || 'current' ) if( !obj ) return;
}; if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
obj.forEach(function(elem){
replaceDatesInObject( elem );
}); });
this.education.history && this.education.history.forEach( function(edu) { }
edu.safe = { else if (typeof obj === 'object') {
start: _fmt( edu.start ), if( obj._isAMomentObject || obj.safe )
end: _fmt( edu.end || 'current' ) return;
}; Object.keys( obj ).forEach(function(key) {
replaceDatesInObject( obj[key] );
}); });
this.service.history && this.service.history.forEach( function(vol) { ['start','end','date'].forEach( function(val) {
vol.safe = { if( obj[val] && (!obj.safe || !obj.safe[val] )) {
start: _fmt( vol.start ), obj.safe = obj.safe || { };
end: _fmt( vol.end || 'current' ) obj.safe[ val ] = _fmt( obj[val] );
}; if( obj[val] && (val === 'start') && !obj.end ) {
obj.safe.end = _fmt('current');
}
}
}); });
this.recognition && this.recognition.forEach( function(rec) { }
rec.safe = { }
date: _fmt( rec.date )
}; Object.keys( this ).forEach(function(member){
}); replaceDatesInObject( that[ member ] );
this.writing && this.writing.forEach( function(pub) {
pub.safe = {
date: _fmt( pub.date )
};
}); });
} }
/** /**

View File

@ -50,7 +50,7 @@ Definition of the JRSResume class.
*/ */
JRSResume.prototype.stringify = function() { JRSResume.prototype.stringify = function() {
function replacer( key,value ) { // Exclude these keys from stringification function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview', 'display_progress_bar'], 'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; } function( val ) { return key.trim() === val; }
@ -94,14 +94,14 @@ Definition of the JRSResume class.
}); });
} }
return flatSkills; return flatSkills;
}, };
/** /**
Update the sheet's raw data. TODO: remove/refactor Update the sheet's raw data. TODO: remove/refactor
*/ */
JRSResume.prototype.updateData = function( str ) { JRSResume.prototype.updateData = function( str ) {
this.clear( false ); this.clear( false );
this.parse( str ) this.parse( str );
return this; return this;
}; };
@ -126,8 +126,8 @@ Definition of the JRSResume class.
Get the default (empty) sheet. Get the default (empty) sheet.
*/ */
JRSResume.default = function() { JRSResume.default = function() {
return new JRSResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); return new JRSResume().open( PATH.join( __dirname, 'empty-jrs.json'), 'Empty' );
} };
/** /**
Add work experience to the sheet. Add work experience to the sheet.
@ -168,7 +168,7 @@ Definition of the JRSResume class.
JRSResume.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓ JRSResume.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓
var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' ); var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' );
var schemaObj = JSON.parse( schema ); var schemaObj = JSON.parse( schema );
var validator = require('is-my-json-valid') var validator = require('is-my-json-valid');
var validate = validator( schemaObj ); var validate = validator( schemaObj );
return validate( this ); return validate( this );
}; };

View File

@ -10,10 +10,12 @@ Abstract theme representation.
, validator = require('is-my-json-valid') , validator = require('is-my-json-valid')
, _ = require('underscore') , _ = require('underscore')
, PATH = require('path') , PATH = require('path')
, moment = require('moment'); , EXTEND = require('../utils/extend')
, moment = require('moment')
, RECURSIVE_READ_DIR = require('recursive-readdir-sync');
/** /**
The Theme class represents a specific presentation of a resume. The Theme class is a representation of a FluentCV theme asset.
@class Theme @class Theme
*/ */
function Theme() { function Theme() {
@ -25,59 +27,38 @@ Abstract theme representation.
*/ */
Theme.prototype.open = function( themeFolder ) { Theme.prototype.open = function( themeFolder ) {
function friendlyName( val ) { // Open the [theme-name].json file; should have the same name as folder
val = val.trim().toLowerCase(); this.folder = themeFolder;
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }; var pathInfo = PATH.parse( themeFolder );
return friendly[val] || val; var themeFile = PATH.join( themeFolder, pathInfo.base + '.json' );
var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) );
var that = this;
// 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 ) {
formatsHash = loadExplicit.call( this );
this.explicit = true;
}
else {
formatsHash = loadImplicit.call( this );
} }
// Remember the theme folder; might be custom
this.folder = themeFolder;
// Iterate over all files in the theme folder, producing an array, fmts,
// containing info for each file.
var tplFolder = PATH.join( themeFolder, 'templates' );
var fmts = FS.readdirSync( tplFolder ).map( function( file ) {
var absPath = PATH.join( tplFolder, file );
var pathInfo = PATH.parse(absPath);
var temp = [ pathInfo.name, {
title: friendlyName(pathInfo.name),
pre: pathInfo.name,
ext: pathInfo.ext.slice(1),
path: absPath,
data: FS.readFileSync( absPath, 'utf8' ),
css: null
}];
return temp;
});
// Add freebie formats every theme gets // Add freebie formats every theme gets
fmts.push( [ 'json', { title: 'json', pre: 'json', ext: 'json', path: null, data: null } ] ); formatsHash.json = { title: 'json', outFormat: 'json', pre: 'json', ext: 'json', path: null, data: null };
fmts.push( [ 'yml', { title: 'yaml', pre: 'yml', ext: 'yml', path: null, data: null } ] ); formatsHash.yml = { title: 'yaml', outFormat: 'yml', pre: 'yml', ext: 'yml', path: null, data: null };
// Now, get all the CSS files... // Cache
this.cssFiles = fmts.filter(function( fmt ){ return fmt[1].ext === 'css'; }); this.formats = formatsHash;
// ...and assemble information on them
this.cssFiles.forEach(function( cssf ) {
// For each CSS file, get its corresponding HTML file
var idx = _.findIndex(fmts, function( fmt ) {
return fmt[1].pre === cssf[1].pre && fmt[1].ext === 'html'
});
fmts[ idx ][1].css = cssf[1].data;
fmts[ idx ][1].cssPath = cssf[1].path;
});
// Remove CSS files from the formats array
fmts = fmts.filter( function( fmt) {
return fmt[1].ext !== 'css';
});
// Create a hash out of the formats for this theme
this.formats = _.object( fmts );
// Set the official theme name // Set the official theme name
this.name = PATH.parse( themeFolder ).name; this.name = PATH.parse( this.folder ).name;
return this; return this;
}; };
@ -96,6 +77,187 @@ Abstract theme representation.
return this.formats[ fmt ]; return this.formats[ fmt ];
}; };
function loadImplicit() {
// Set up a hash of formats supported by this theme.
var formatsHash = { };
var that = this;
var major = false;
// Establish the base theme folder
var tplFolder = PATH.join( this.folder, 'src' );
// 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 ) {
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
var pathInfo = PATH.parse(absPath);
var outFmt = '', isMajor = false;
var portion = pathInfo.dir.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
var res = reg.exec( portion );
res && (outFmt = res[1]);
}
// Otherwise, the output format is inferred from the filename, as in
// 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 );
isMajor = true;
}
// We should have a valid output format now.
formatsHash[ outFmt ] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
};
// Create the file representation object.
var obj = {
action: 'transform',
path: absPath,
major: isMajor,
orgPath: PATH.relative(that.folder, absPath),
ext: pathInfo.ext.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
data: FS.readFileSync( absPath, 'utf8' ),
css: null
};
// Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj );
return obj;
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; }))
.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';
});
cssf.action = null;
fmts[ idx ].css = cssf.data;
fmts[ idx ].cssPath = cssf.path;
});
// Remove CSS files from the formats array
fmts = fmts.filter( function( fmt) {
return fmt.ext !== 'css';
});
return formatsHash;
}
function loadExplicit() {
var that = this;
// Set up a hash of formats supported by this theme.
var formatsHash = { };
// Establish the base theme folder
var tplFolder = PATH.join( this.folder, 'src' );
var act = null;
// 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 ) {
act = null;
// If this file is mentioned in the theme's JSON file under "transforms"
var pathInfo = PATH.parse(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;
});
});
if( outFmt ) {
act = 'transform';
}
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
if( !outFmt ) {
var portion = pathInfo.dir.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
var res = reg.exec( portion );
res && (outFmt = res[1]);
}
}
// Otherwise, the output format is inferred from the filename, as in
// 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 );
}
// We should have a valid output format now.
formatsHash[ outFmt ] =
formatsHash[ outFmt ] || {
outFormat: outFmt,
files: [],
symLinks: that.formats[ outFmt ].symLinks
};
// Create the file representation object.
var obj = {
action: act,
orgPath: PATH.relative(that.folder, absPath),
path: absPath,
ext: pathInfo.ext.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
data: FS.readFileSync( absPath, 'utf8' ),
css: null
};
// Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj );
return obj;
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; }))
.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';
});
fmts[ idx ].css = cssf.data;
fmts[ idx ].cssPath = cssf.path;
});
// Remove CSS files from the formats array
fmts = fmts.filter( function( fmt) {
return fmt.ext !== 'css';
});
return formatsHash;
}
function friendlyName( val ) {
val = val.trim().toLowerCase();
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val;
}
module.exports = Theme; module.exports = Theme;
}()); }());

View File

@ -0,0 +1,18 @@
/**
Handlebars template generate for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
*/
(function() {
var _ = require('underscore');
var HANDLEBARS = require('handlebars');
module.exports = function( json, jst, format, cssInfo, opts ) {
var template = HANDLEBARS.compile(jst);
return template( { r: json, filt: opts.filters, cssInfo: cssInfo, headFragment: opts.headFragment || '' } );
};
}());

View File

@ -0,0 +1,37 @@
/**
Underscore template generate for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
*/
(function() {
var _ = require('underscore');
module.exports = function( json, jst, format, cssInfo, opts ) {
// Tweak underscore's default template delimeters
var delims = opts.themeObj.delimeters || opts.template;
if( 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;
};
}());

View File

@ -4,9 +4,9 @@ Internal resume generation logic for FluentCV.
@module fluentcmd.js @module fluentcmd.js
*/ */
(function() {
module.exports = function () { module.exports = function () {
// We don't mind pseudo-globals here
var path = require( 'path' ) var path = require( 'path' )
, extend = require( './utils/extend' ) , extend = require( './utils/extend' )
, unused = require('./utils/string') , unused = require('./utils/string')
@ -42,8 +42,8 @@ module.exports = function () {
// Merge input resumes... // Merge input resumes...
var msg = ''; var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) { rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ? 'Merging '.gray + a.imp.fileName : '') msg += ((idx == sheets.length - 2) ?
+ ' onto '.gray + b.imp.fileName; 'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName;
return extend( true, b, a ); return extend( true, b, a );
}); });
msg && _log(msg); msg && _log(msg);
@ -62,8 +62,8 @@ module.exports = function () {
// Load the theme // Load the theme
var theTheme = new FLUENT.Theme().open( tFolder ); var theTheme = new FLUENT.Theme().open( tFolder );
_opts.themeObj = theTheme; _opts.themeObj = theTheme;
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + (' theme (' + _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
Object.keys(theTheme.formats).length + ' formats)').info ); (' theme (' +Object.keys(theTheme.formats).length + ' formats)').info);
// Expand output resumes... (can't use map() here) // Expand output resumes... (can't use map() here)
var targets = [], that = this; var targets = [], that = this;
@ -76,7 +76,7 @@ module.exports = function () {
targets.push.apply(targets, fmat === '.all' ? targets.push.apply(targets, fmat === '.all' ?
Object.keys( theTheme.formats ).map(function(k){ Object.keys( theTheme.formats ).map(function(k){
var z = theTheme.formats[k]; var z = theTheme.formats[k];
return { file: to.replace(/all$/g,z.pre), fmt: z } 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) ) }]);
}); });
@ -93,19 +93,53 @@ module.exports = function () {
@param f Full path to the destination resume to generate, for example, @param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt". "/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/ */
function single( fi, theme ) { function single( targInfo, theme ) {
try { try {
var f = fi.file, fType = fi.fmt.ext, fName = path.basename(f,'.'+fType); var f = targInfo.file
var fObj = _.property( fi.fmt.pre )( theme.formats ); , fType = targInfo.fmt.outFormat
var fOut = path.join( f.substring( 0, f.lastIndexOf('.')+1 ) + fObj.pre); , fName = path.basename(f, '.' + fType)
, theFormat;
_log( 'Generating '.useful + fi.fmt.title.toUpperCase().useful.bold + ' resume: '.useful + // If targInfo.fmt.files exists, this theme has an explicit "files"
path.relative(process.cwd(), f ).useful.bold ); // section in its theme.json file.
if( targInfo.fmt.files && targInfo.fmt.files.length ) {
var theFormat = _fmts.filter( _log( 'Generating '.useful +
function( fmt ) { return fmt.name === fi.fmt.pre; })[0]; targInfo.fmt.outFormat.toUpperCase().useful.bold +
MKDIRP.sync( path.dirname(fOut) ); // Ensure dest folder exists; ' resume: '.useful + path.relative(process.cwd(), f ).useful.bold);
theFormat.gen.generate( rez, fOut, _opts );
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 );
// 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 {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + path.relative(process.cwd(), f ).useful.bold);
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 );
}
} }
catch( ex ) { catch( ex ) {
_err( ex ); _err( ex );
@ -148,15 +182,18 @@ module.exports = function () {
sheets.forEach( function( rep ) { sheets.forEach( function( rep ) {
var rez;
try { try {
var rez = JSON.parse( rep.raw ); rez = JSON.parse( rep.raw );
} }
catch( ex ) { catch( ex ) {
_log('Validating '.info + rep.file.infoBold + ' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold); _log('Validating '.info + rep.file.infoBold +
' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold);
if (ex instanceof SyntaxError) { if (ex instanceof SyntaxError) {
// Invalid JSON // Invalid JSON
_log( '--> '.bold.red + rep.file.toUpperCase().red + ' contains invalid JSON. Unable to validate.'.red ); _log( '--> '.bold.red + rep.file.toUpperCase().red +
' contains invalid JSON. Unable to validate.'.red );
_log( (' INTERNAL: ' + ex).red ); _log( (' INTERNAL: ' + ex).red );
} }
else { else {
@ -169,14 +206,15 @@ module.exports = function () {
var isValid = false; var isValid = false;
var style = 'useful'; var style = 'useful';
var errors = []; var errors = [];
var fmt = rez.meta &&
(rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars';
try { try {
var fmt = rez.meta && rez.meta.format === 'FRESH@0.1.0' ? 'fresh':'jars';
var validate = validator( schemas[ fmt ], { // Note [1] var validate = validator( schemas[ fmt ], { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
}); });
isValid = validate( rez ); isValid = validate( rez );
@ -191,15 +229,15 @@ module.exports = function () {
} }
_log( 'Validating '.info + rep.file.infoBold + ' against '.info + _log( 'Validating '.info + rep.file.infoBold + ' against '.info +
fmt.replace('jars','JSON Resume').toUpperCase().infoBold + ' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold ); fmt.replace('jars','JSON Resume').toUpperCase().infoBold +
' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold );
errors.forEach(function(err,idx) { errors.forEach(function(err,idx) {
_log( '--> '.bold.yellow + ( err.field.replace('data.','resume.').toUpperCase() _log( '--> '.bold.yellow +
+ ' ' + err.message).yellow ); (err.field.replace('data.','resume.').toUpperCase() + ' ' +
err.message).yellow );
}); });
}); });
} }
@ -221,22 +259,40 @@ module.exports = function () {
sheets.forEach(function(sheet, idx){ sheets.forEach(function(sheet, idx){
var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH';
var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS';
_log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' + sourceFormat + ') to ').useful + dst[0].useful.bold + _log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' +
sourceFormat + ') to ').useful + dst[0].useful.bold +
(' (' + targetFormat + ').').useful ); (' (' + targetFormat + ').').useful );
sheet.saveAs( dst[idx], targetFormat ); sheet.saveAs( dst[idx], targetFormat );
}); });
} }
/**
Create a new empty resume in either FRESH or JRS format.
*/
function create( src, dst, opts, logger ) {
_log = logger || console.log;
dst = src || ['resume.json'];
dst.forEach( function( t ) {
var safeFormat = opts.format.toUpperCase();
_log('Creating new '.useful +safeFormat.useful.bold +
' resume: '.useful + t.useful.bold);
MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists;
FLUENT[ safeFormat + 'Resume' ].default().save( t );
});
}
/** /**
Display help documentation. Display help documentation.
*/ */
function help() { function help() {
console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).useful.bold ); console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' )
.useful.bold );
} }
function loadSourceResumes( src, fn ) { function loadSourceResumes( src, fn ) {
return src.map( function( res ) { return src.map( function( res ) {
_log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info + res.cyan.bold ); _log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info +
res.cyan.bold );
return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res ); return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res );
}); });
} }
@ -251,7 +307,8 @@ module.exports = function () {
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() }, { name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() }, { name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() },
{ name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() }, { name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() } { name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() }
]; ];
/** /**
@ -275,6 +332,7 @@ module.exports = function () {
build: generate, build: generate,
validate: validate, validate: validate,
convert: convert, convert: convert,
new: create,
help: help help: help
}, },
lib: require('./fluentlib'), lib: require('./fluentlib'),
@ -284,5 +342,7 @@ module.exports = function () {
}(); }();
}());
// [1]: JSON.parse throws SyntaxError on invalid JSON. See: // [1]: JSON.parse throws SyntaxError on invalid JSON. See:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse

View File

@ -16,5 +16,6 @@ module.exports = {
MarkdownGenerator: require('./gen/markdown-generator'), MarkdownGenerator: require('./gen/markdown-generator'),
JsonGenerator: require('./gen/json-generator'), JsonGenerator: require('./gen/json-generator'),
YamlGenerator: require('./gen/yaml-generator'), YamlGenerator: require('./gen/yaml-generator'),
JsonYamlGenerator: require('./gen/json-yaml-generator') JsonYamlGenerator: require('./gen/json-yaml-generator'),
LaTeXGenerator: require('./gen/latex-generator')
}; };

View File

@ -21,12 +21,12 @@ HTML resume generator for FluentCV.
the HTML resume prior to saving. the HTML resume prior to saving.
*/ */
onBeforeSave: function( info ) { onBeforeSave: function( info ) {
var cssSrc = PATH.join( info.theme.folder, 'templates', '*.css' ) var cssSrc = PATH.join( info.theme.folder, 'src', '*.css' )
, outFolder = PATH.parse( info.outputFile ).dir, that = this; , outFolder = PATH.parse( info.outputFile ).dir, that = this;
info.theme.cssFiles.forEach( function( f ) { info.theme.cssFiles.forEach( function( f ) {
var fi = PATH.parse( f[1].path ); var fi = PATH.parse( f.path );
FS.copySync( f[1].path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) { FS.copySync( f.path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) {
throw { fluenterror: that.codes.copyCss, data: [cssSrc,cssDst] }; throw { fluenterror: that.codes.copyCss, data: [cssSrc,cssDst] };
}); });
}); });

View File

@ -0,0 +1,17 @@
/**
LaTeX resume generator for FluentCV.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
*/
var TemplateGenerator = require('./template-generator');
/**
LaTeXGenerator generates a LaTeX resume via TemplateGenerator.
*/
var LaTeXGenerator = module.exports = TemplateGenerator.extend({
init: function(){
this._super( 'latex', 'tex' );
}
});

View File

@ -1,21 +1,29 @@
/** /**
Template-based resume generator base for FluentCV. Template-based resume generator base for FluentCV.
@license Copyright (c) 2015 | James M. Devlin @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
*/ */
var FS = require( 'fs' ) (function() {
var FS = require( 'fs-extra' )
, _ = require( 'underscore' ) , _ = require( 'underscore' )
, MD = require( 'marked' ) , MD = require( 'marked' )
, XML = require( 'xml-escape' ) , XML = require( 'xml-escape' )
, PATH = require('path') , PATH = require('path')
, MKDIRP = require('mkdirp')
, BaseGenerator = require( './base-generator' ) , BaseGenerator = require( './base-generator' )
, EXTEND = require('../utils/extend') , EXTEND = require('../utils/extend')
, Theme = require('../core/theme'); , Theme = require('../core/theme');
// Default options. // Default options.
var _defaultOpts = { var _defaultOpts = {
engine: 'underscore',
keepBreaks: true, keepBreaks: true,
freezeBreaks: true, freezeBreaks: false,
nSym: '&newl;', // newline entity nSym: '&newl;', // newline entity
rSym: '&retn;', // return entity rSym: '&retn;', // return entity
template: { template: {
@ -28,11 +36,11 @@ var _defaultOpts = {
out: function( txt ) { return txt; }, out: function( txt ) { return txt; },
raw: function( txt ) { return txt; }, raw: function( txt ) { return txt; },
xml: function( txt ) { return XML(txt); }, xml: function( txt ) { return XML(txt); },
md: function( txt ) { return MD(txt); }, md: function( txt ) { return MD( txt || '' ); },
mdin: function( txt ) { return MD(txt).replace(/^\s*\<p\>|\<\/p\>\s*$/gi, ''); }, mdin: function( txt ) { return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); },
lower: function( txt ) { return txt.toLowerCase(); }, lower: function( txt ) { return txt.toLowerCase(); },
link: function( name, url ) { return url ? link: function( name, url ) { return url ?
'<a href="' + url + '">' + name + '</a>' : name } '<a href="' + url + '">' + name + '</a>' : name; }
}, },
prettify: { // ← See https://github.com/beautify-web/js-beautify#options prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2, indent_size: 2,
@ -42,38 +50,118 @@ var _defaultOpts = {
} }
}; };
/** /**
TemplateGenerator performs resume generation via Underscore-style template TemplateGenerator performs resume generation via local Handlebar or Underscore
expansion and is appropriate for text-based formats like HTML, plain text, style template expansion and is appropriate for text-based formats like HTML,
and XML versions of Microsoft Word, Excel, and OpenOffice. plain text, and XML versions of Microsoft Word, Excel, and OpenOffice.
@class TemplateGenerator
*/ */
var TemplateGenerator = module.exports = BaseGenerator.extend({ var TemplateGenerator = module.exports = BaseGenerator.extend({
/** outputFormat: html, txt, pdf, doc
templateFormat: html or txt
**/
init: function( outputFormat, templateFormat, cssFile ){ init: function( outputFormat, templateFormat, cssFile ){
this._super( outputFormat ); this._super( outputFormat );
this.tplFormat = templateFormat || outputFormat; this.tplFormat = templateFormat || outputFormat;
}, },
/** Default generation method for template-based generators. */
invoke: function( rez, themeMarkup, cssInfo, opts ) {
// Compile and invoke the template!
invoke: function( rez, themeMarkup, cssInfo, opts ) {
this.opts = EXTEND( true, {}, _defaultOpts, opts ); this.opts = EXTEND( true, {}, _defaultOpts, opts );
mk = this.single( rez, themeMarkup, this.format, cssInfo, { } ); mk = this.single( rez, themeMarkup, this.format, cssInfo, { } );
this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f )); this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f ));
return mk; return mk;
}, },
/** Default generation method for template-based generators. */
/**
Default generation method for template-based generators.
@method generate
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options.
*/
generate: function( rez, f, opts ) { generate: function( rez, f, opts ) {
// Carry over options // Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts ); this.opts = EXTEND( true, { }, _defaultOpts, opts );
// Load the theme
var themeInfo = themeFromMoniker.call( this );
var theme = themeInfo.theme;
var tFolder = themeInfo.folder;
var tplFolder = PATH.join( tFolder, 'src' );
var outFolder = PATH.parse(f).dir;
var curFmt = theme.getFormat( this.format );
var that = this;
// "Generate": process individual files within the theme
curFmt.files.forEach(function(tplInfo){
if( tplInfo.action === 'transform' ) {
transform.call( that, rez, f, tplInfo, theme, outFolder );
}
else if( tplInfo.action === null && theme.explicit ) {
var thisFilePath = PATH.join(outFolder, tplInfo.orgPath);
try {
MKDIRP.sync( PATH.dirname(thisFilePath) );
FS.copySync( tplInfo.path, thisFilePath );
}
catch(ex) {
console.log(ex);
}
}
});
// Some themes require a symlink structure. If so, create it.
if( curFmt.symLinks ) {
Object.keys( curFmt.symLinks ).forEach( function(loc) {
var absLoc = PATH.join(outFolder, loc);
var absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
var type = PATH.parse( absLoc ).ext ? 'file' : 'junction'; // 'file', 'dir', or 'junction' (Windows only)
FS.symlinkSync( absTarg, absLoc, type);
});
}
},
/**
Perform a single resume JSON-to-DEST resume transformation.
@param json A FRESH or JRS resume object.
@param jst The stringified template data
@param format The format name, such as "html" or "latex"
@param cssInfo Needs to be refactored.
@param opts Options and passthrough data.
*/
single: function( json, jst, format, cssInfo, opts ) {
this.opts.freezeBreaks && ( jst = freeze(jst) );
var eng = require( '../eng/' + opts.themeObj.engine + '-generator' );
var result = eng( json, jst, format, cssInfo, opts );
this.opts.freezeBreaks && ( result = unfreeze(result) );
return result;
}
});
/**
Export the TemplateGenerator function/ctor.
*/
module.exports = TemplateGenerator;
/**
Given a theme title, load the corresponding theme.
*/
function themeFromMoniker() {
// Verify the specified theme name/path // Verify the specified theme name/path
var tFolder = PATH.join( var tFolder = PATH.join(
PATH.parse( require.resolve('fluent-themes') ).dir, PATH.parse( require.resolve('fluent-themes') ).dir,
@ -87,65 +175,35 @@ var TemplateGenerator = module.exports = BaseGenerator.extend({
} }
} }
// Load the theme var t = this.opts.themeObj || new Theme().open( tFolder );
var theme = opts.themeObj || new Theme().open( tFolder );
// Load theme and CSS data // Load the theme and format
var tplFolder = PATH.join( tFolder, 'templates' ); return {
var curFmt = theme.getFormat( this.format ); theme: t,
var cssInfo = { file: curFmt.css ? curFmt.cssPath : null, data: curFmt.css || null }; folder: tFolder
};
}
// Compile and invoke the template!
var mk = this.single( rez, curFmt.data, this.format, cssInfo, opts );
/**
Transform a single subfile.
*/
function transform( rez, f, tplInfo, theme, outFolder ) {
var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null };
var mk = this.single( rez, tplInfo.data, this.format, cssInfo, this.opts );
this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } ));
FS.writeFileSync( f, mk, { encoding: 'utf8', flags: 'w' } ); var thisFilePath = PATH.join( outFolder, tplInfo.orgPath );
try {
}, MKDIRP.sync( PATH.dirname( tplInfo.major ? f : thisFilePath) );
FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } );
/**
Perform a single resume JSON-to-DEST resume transformation. Exists as a
separate function in order to expose string-based transformations to clients
who don't have access to filesystem resources (in-browser, etc.).
*/
single: function( json, jst, format, cssInfo, opts ) {
// Freeze whitespace in the template.
this.opts.freezeBreaks && ( jst = freeze(jst) );
// Tweak underscore's default template delimeters
_.templateSettings = this.opts.template;
// Convert {{ someVar }} to {% print(filt.out(someVar) %}
// Convert {{ someVar|someFilter }} to {% print(filt.someFilter(someVar) %}
jst = jst.replace( _.templateSettings.interpolate, function replace(m, p1) {
if( p1.indexOf('|') > -1 ) {
var terms = p1.split('|');
return '{% print( filt.' + terms[1] + '( ' + terms[0] + ' )) %}';
} }
else { catch(ex) {
return '{% print( filt.out(' + p1 + ') ) %}'; console.log(ex);
} }
});
// Strip {# comments #}
jst = jst.replace( _.templateSettings.comment, '');
// Compile and run the template. TODO: avoid unnecessary recompiles.
jst = _.template(jst)({ r: json, filt: this.opts.filters, cssInfo: cssInfo, headFragment: this.opts.headFragment || '' });
// Unfreeze whitespace
this.opts.freezeBreaks && ( jst = unfreeze(jst) );
return jst;
} }
});
/**
Export the TemplateGenerator function/ctor.
*/
module.exports = TemplateGenerator;
/** /**
Freeze newlines for protection against errant JST parsers. Freeze newlines for protection against errant JST parsers.
@ -156,6 +214,8 @@ function freeze( markup ) {
.replace( _reg.regR, _defaultOpts.rSym ); .replace( _reg.regR, _defaultOpts.rSym );
} }
/** /**
Unfreeze newlines when the coast is clear. Unfreeze newlines when the coast is clear.
*/ */
@ -165,6 +225,8 @@ function unfreeze( markup ) {
.replace( _reg.regSymN, '\n' ); .replace( _reg.regSymN, '\n' );
} }
/** /**
Regexes for linebreak preservation. Regexes for linebreak preservation.
*/ */
@ -174,3 +236,7 @@ var _reg = {
regSymN: new RegExp( _defaultOpts.nSym, 'g' ), regSymN: new RegExp( _defaultOpts.nSym, 'g' ),
regSymR: new RegExp( _defaultOpts.rSym, 'g' ) regSymR: new RegExp( _defaultOpts.rSym, 'g' )
}; };
}());

View File

@ -59,17 +59,14 @@ function main() {
var splitAt = _.indexOf( params, 'to' ); var splitAt = _.indexOf( params, 'to' );
if( splitAt === a._.length - 1 ) { if( splitAt === a._.length - 1 ) {
// 'TO' cannot be the last argument // 'TO' cannot be the last argument
logMsg('Please '.warn + 'specify an output file'.warnBold + logMsg('Please '.warn + 'specify an output file'.warn.bold +
' for this operation or '.warn + 'omit the TO keyword'.warnBold + '.'.warn ); ' for this operation or '.warn + 'omit the TO keyword'.warn.bold +
'.'.warn );
return; return;
} }
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt ); var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 ); var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 );
// Preload our params array
//var dst = (a.o && ((typeof a.o === 'string' && [ a.o ]) || a.o)) || [];
//dst = (dst === true) ? [] : dst; // Handle -o with missing output file
var parms = [ src, dst, opts, logMsg ]; var parms = [ src, dst, opts, logMsg ];
// Invoke the action // Invoke the action
@ -82,10 +79,11 @@ function logMsg( msg ) {
} }
function getOpts( args ) { function getOpts( args ) {
var noPretty = args['nopretty'] || args.n; var noPretty = args.nopretty || args.n;
noPretty = noPretty && (noPretty === true || noPretty === 'true'); noPretty = noPretty && (noPretty === true || noPretty === 'true');
return { return {
theme: args.t || 'modern', theme: args.t || 'modern',
format: args.f || 'FRESH',
prettify: !noPretty, prettify: !noPretty,
silent: args.s || args.silent silent: args.s || args.silent
}; };
@ -103,15 +101,15 @@ function handleError( ex ) {
case 3: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in FRESH or JSON Resume format.'.guide; 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 4: msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide +
Object.keys( FCMD.verbs ).map( function(v, idx, ar) { Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? 'or '.guide : '') return (idx === ar.length - 1 ? 'or '.guide : '') +
+ v.toUpperCase().guide; v.toUpperCase().guide;
}).join(', '.guide) + ") to get started.\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; break;
//case 4: msg = title + '\n' + ; break; //case 4: msg = title + '\n' + ; break;
case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break; case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break;
case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.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 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break;
}; }
exitCode = ex.fluenterror; exitCode = ex.fluenterror;
} }

View File

@ -1,22 +1,25 @@
Usage: Usage:
fluentcv <COMMAND> <SOURCES> [TO <TARGETS>] [-t <THEME>] fluentcv <COMMAND> <SOURCES> [TO <TARGETS>] [-t <THEME>] [-f <FORMAT>]
<COMMAND> should be BUILD, CONVERT, VALIDATE, or HELP. <SOURCES> should <COMMAND> should be BUILD, NEW, CONVERT, VALIDATE, or HELP. <SOURCES> should
be the path to one or more FRESH or JSON Resume format resumes. <TARGETS> be the path to one or more FRESH or JSON Resume format resumes. <TARGETS>
should be the name of the destination resume to be created, if any. The should be the name of the destination resume to be created, if any. The
<THEME> parameter should be the name of a predefined theme (for example: <THEME> parameter should be the name of a predefined theme (for example:
COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a custom
custom theme. theme. <FORMAT> should be either FRESH (for a FRESH-format resume) or JRS
(for a JSON Resume-format resume).
fluentcv BUILD resume.json TO out/resume.all fluentcv BUILD resume.json TO out/resume.all
fluentcv NEW resume.json
fluentcv CONVERT resume.json TO resume-jrs.json fluentcv CONVERT resume.json TO resume-jrs.json
fluentcv VALIDATE resume.json fluentcv VALIDATE resume.json
Both SOURCES and TARGETS can accept multiple files: Both SOURCES and TARGETS can accept multiple files:
fluentCV BUILD r1.json r2.json TO out/resume.all out2/resume.html fluentCV BUILD r1.json r2.json TO out/resume.all out2/resume.html
fluentCV NEW r1.json r2.json r3.json
fluentCV VALIDATE resume.json resume2.json resume3.json fluentCV VALIDATE resume.json resume2.json resume3.json
See https://github.com/fluentdesk/fluentCV/blob/master/README.md See https://github.com/fluentdesk/fluentCV/blob/master/README.md for more
for more information. information.

View File

@ -41,7 +41,7 @@
return ret; return ret;
}; };
})(name, prop[name]) : })(name, prop[name]) : // jshint ignore:line
prop[name]; prop[name];
} }

110
tests/test-themes.js Normal file
View File

@ -0,0 +1,110 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, FCMD = require( '../src/fluentcmd')
, validator = require('is-my-json-valid')
, COLORS = require('colors');
chai.config.includeStack = false;
describe('Testing themes', function () {
var _sheet;
COLORS.setTheme({
title: ['white','bold'],
info: process.platform === 'win32' ? 'gray' : ['white','dim'],
infoBold: ['white','dim'],
warn: 'yellow',
error: 'red',
guide: 'yellow',
status: 'gray',//['white','dim'],
useful: 'green',
});
it('HELLO-WORLD 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 opts = {
theme: 'hello-world',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
it('COMPACT theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/compact/resume.all'];
var opts = {
theme: 'compact',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
it('MODERN theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/modern/resume.all'];
var opts = {
theme: 'modern',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
it('MINIMIST theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/minimist/resume.all'];
var opts = {
theme: 'minimist',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
it('AWESOME theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/awesome/resume.all'];
var opts = {
theme: 'awesome',
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
});
// describe('subtract', function () {
// it('should return -1 when passed the params (1, 2)', function () {
// expect(math.subtract(1, 2)).to.equal(-1);
// });
// });