1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-11-25 09:50: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) {
'use strict';
var opts = {
pkg: grunt.file.readJSON('package.json'),
@ -29,19 +29,28 @@ module.exports = function (grunt) {
outdir: 'docs/'
}
}
},
jshint: {
options: {
laxcomma: true,
expr: true
},
all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js']
}
};
grunt.initConfig( opts );
grunt.loadNpmTasks('grunt-simple-mocha');
grunt.loadNpmTasks('grunt-contrib-yuidoc');
grunt.registerTask('test', 'Test the FluentCV library.', function( config ) {
grunt.task.run( ['simplemocha:all'] );
});
grunt.registerTask('document', 'Generate FluentCV library documentation.', function( config ) {
grunt.task.run( ['yuidoc'] );
});
grunt.registerTask('default', [ 'test', 'yuidoc' ]);
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.registerTask('test', 'Test the FluentCV library.',
function( config ) { grunt.task.run( ['simplemocha:all'] ); });
grunt.registerTask('document', 'Generate FluentCV library documentation.',
function( config ) { grunt.task.run( ['yuidoc'] ); });
grunt.registerTask('default', [ 'jshint', 'test', 'yuidoc' ]);
};

View File

@ -1,7 +1,7 @@
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
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,47 +1,51 @@
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)
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
text, JSON, and YAML formats—without violating DRY.
1. **Generate** HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML,
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.
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
[sudo] npm install fluentcv -g
```
Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for
your platform.
## 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].
your platform. For LaTeX generation you'll need a valid LaTeX environment with
access to `xelatex` and similar.
## Getting Started
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.
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
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
```
- `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.
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.
Markdown | .md | A structured Markdown document that can be used as-is or used to generate HTML.
LaTeX | .tex | A structured LaTeX document (or collection of documents).
MS Word | .doc | A Microsoft Word office document.
Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme.
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:
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`.
3. You're ready to go.

View File

@ -1,6 +1,6 @@
{
"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.",
"repository": {
"type": "git",
@ -32,10 +32,11 @@
},
"homepage": "https://github.com/fluentdesk/fluentcv",
"dependencies": {
"FRESCA": "fluentdesk/FRESCA#v0.1.0",
"fresca": "^0.2.0",
"colors": "^1.1.2",
"fluent-themes": "0.5.1-beta",
"fluent-themes": "0.6.0-beta",
"fs-extra": "^0.24.0",
"handlebars": "^4.0.5",
"html": "0.0.10",
"is-my-json-valid": "^2.12.2",
"jst": "0.0.13",
@ -43,6 +44,7 @@
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"moment": "^2.10.6",
"recursive-readdir-sync": "^1.0.6",
"underscore": "^1.8.3",
"wkhtmltopdf": "^0.1.5",
"xml-escape": "^1.0.0",
@ -51,6 +53,7 @@
"devDependencies": {
"chai": "*",
"grunt": "*",
"grunt-contrib-jshint": "^0.11.3",
"grunt-contrib-yuidoc": "^0.10.0",
"grunt-simple-mocha": "*",
"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"
var parts = dt.split(' ');
var month = (months[parts[0]] || abbr[parts[0]]);
var dt = parts[1] + '-' + (month < 10 ? '0' + month : month.toString());
return moment( dt, 'YYYY-MM' );
var temp = parts[1] + '-' + (month < 10 ? '0' + month : month.toString());
return moment( temp, 'YYYY-MM' );
}
else if( /^\d{4}-\d{1,2}$/.test(dt) ) { // "2015-03", "1998-4"
return moment( dt, 'YYYY-MM' );

View File

@ -11,6 +11,7 @@ Definition of the FRESHResume class.
, _ = require('underscore')
, PATH = require('path')
, moment = require('moment')
, MD = require('marked')
, CONVERTER = require('./convert');
/**
@ -54,7 +55,13 @@ Definition of the FRESHResume class.
FS.writeFileSync( this.imp.fileName, FreshResume.stringify( newRep ), 'utf8' );
}
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
@ -68,7 +75,60 @@ Definition of the FRESHResume class.
) ? undefined : value;
}
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
@ -129,7 +189,7 @@ Definition of the FRESHResume class.
*/
FreshResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str )
this.parse( str );
return this;
};
@ -143,9 +203,10 @@ Definition of the FRESHResume class.
delete this.employment;
delete this.service;
delete this.education;
//delete this.awards;
delete this.publications;
//delete this.interests;
delete this.recognition;
delete this.reading;
delete this.writing;
delete this.interests;
delete this.skills;
delete this.social;
};
@ -154,8 +215,9 @@ Definition of the FRESHResume class.
Get the default (empty) sheet.
*/
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.
@ -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.
*/
@ -195,7 +278,7 @@ Definition of the FRESHResume class.
*/
FreshResume.prototype.isValid = function( info ) {
var schemaObj = require('FRESCA');
var validator = require('is-my-json-valid')
var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // Note [1]
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
// : ( 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
: ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0;
});
@ -265,35 +348,39 @@ Definition of the FRESHResume class.
function _parseDates() {
var _fmt = require('./fluent-date').fmt;
var that = this;
this.employment.history && this.employment.history.forEach( function(job) {
job.safe = {
start: _fmt( job.start ),
end: _fmt( job.end || 'current' )
};
// TODO: refactor recursion
function replaceDatesInObject( obj ) {
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 = {
start: _fmt( edu.start ),
end: _fmt( edu.end || 'current' )
};
}
else if (typeof obj === 'object') {
if( obj._isAMomentObject || obj.safe )
return;
Object.keys( obj ).forEach(function(key) {
replaceDatesInObject( obj[key] );
});
this.service.history && this.service.history.forEach( function(vol) {
vol.safe = {
start: _fmt( vol.start ),
end: _fmt( vol.end || 'current' )
};
['start','end','date'].forEach( function(val) {
if( obj[val] && (!obj.safe || !obj.safe[val] )) {
obj.safe = obj.safe || { };
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 )
};
});
this.writing && this.writing.forEach( function(pub) {
pub.safe = {
date: _fmt( pub.date )
};
}
}
Object.keys( this ).forEach(function(member){
replaceDatesInObject( that[ member ] );
});
}
/**

View File

@ -50,7 +50,7 @@ Definition of the JRSResume class.
*/
JRSResume.prototype.stringify = function() {
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',
'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; }
@ -94,14 +94,14 @@ Definition of the JRSResume class.
});
}
return flatSkills;
},
};
/**
Update the sheet's raw data. TODO: remove/refactor
*/
JRSResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str )
this.parse( str );
return this;
};
@ -126,8 +126,8 @@ Definition of the JRSResume class.
Get the default (empty) sheet.
*/
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.
@ -168,7 +168,7 @@ Definition of the JRSResume class.
JRSResume.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓
var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' );
var schemaObj = JSON.parse( schema );
var validator = require('is-my-json-valid')
var validator = require('is-my-json-valid');
var validate = validator( schemaObj );
return validate( this );
};

View File

@ -10,10 +10,12 @@ Abstract theme representation.
, validator = require('is-my-json-valid')
, _ = require('underscore')
, 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
*/
function Theme() {
@ -25,59 +27,38 @@ Abstract theme representation.
*/
Theme.prototype.open = function( themeFolder ) {
function friendlyName( val ) {
val = val.trim().toLowerCase();
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val;
// Open the [theme-name].json file; should have the same name as folder
this.folder = themeFolder;
var pathInfo = PATH.parse( themeFolder );
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
fmts.push( [ 'json', { title: 'json', pre: 'json', ext: 'json', path: null, data: null } ] );
fmts.push( [ 'yml', { title: 'yaml', pre: 'yml', ext: 'yml', path: null, data: null } ] );
formatsHash.json = { title: 'json', outFormat: 'json', pre: 'json', ext: 'json', path: null, data: null };
formatsHash.yml = { title: 'yaml', outFormat: 'yml', pre: 'yml', ext: 'yml', path: null, data: null };
// Now, get all the CSS files...
this.cssFiles = fmts.filter(function( fmt ){ return fmt[1].ext === 'css'; });
// ...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 );
// Cache
this.formats = formatsHash;
// Set the official theme name
this.name = PATH.parse( themeFolder ).name;
this.name = PATH.parse( this.folder ).name;
return this;
};
@ -96,6 +77,187 @@ Abstract theme representation.
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;
}());

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
*/
(function() {
module.exports = function () {
// We don't mind pseudo-globals here
var path = require( 'path' )
, extend = require( './utils/extend' )
, unused = require('./utils/string')
@ -42,8 +42,8 @@ module.exports = function () {
// Merge input resumes...
var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ? 'Merging '.gray + a.imp.fileName : '')
+ ' onto '.gray + b.imp.fileName;
msg += ((idx == sheets.length - 2) ?
'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName;
return extend( true, b, a );
});
msg && _log(msg);
@ -62,8 +62,8 @@ module.exports = function () {
// Load the theme
var theTheme = new FLUENT.Theme().open( tFolder );
_opts.themeObj = theTheme;
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + (' theme (' +
Object.keys(theTheme.formats).length + ' formats)').info );
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
(' theme (' +Object.keys(theTheme.formats).length + ' formats)').info);
// Expand output resumes... (can't use map() here)
var targets = [], that = this;
@ -76,7 +76,7 @@ module.exports = function () {
targets.push.apply(targets, fmat === '.all' ?
Object.keys( theTheme.formats ).map(function(k){
var z = theTheme.formats[k];
return { file: to.replace(/all$/g,z.pre), fmt: z }
return { file: to.replace(/all$/g,z.outFormat), fmt: z };
}) : [{ 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,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function single( fi, theme ) {
function single( targInfo, theme ) {
try {
var f = fi.file, fType = fi.fmt.ext, fName = path.basename(f,'.'+fType);
var fObj = _.property( fi.fmt.pre )( theme.formats );
var fOut = path.join( f.substring( 0, f.lastIndexOf('.')+1 ) + fObj.pre);
var f = targInfo.file
, fType = targInfo.fmt.outFormat
, fName = path.basename(f, '.' + fType)
, theFormat;
_log( 'Generating '.useful + fi.fmt.title.toUpperCase().useful.bold + ' resume: '.useful +
path.relative(process.cwd(), f ).useful.bold );
// If targInfo.fmt.files exists, this theme has an explicit "files"
// section in its theme.json file.
if( targInfo.fmt.files && targInfo.fmt.files.length ) {
var theFormat = _fmts.filter(
function( fmt ) { return fmt.name === fi.fmt.pre; })[0];
MKDIRP.sync( path.dirname(fOut) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, fOut, _opts );
_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 );
// 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 ) {
_err( ex );
@ -148,15 +182,18 @@ module.exports = function () {
sheets.forEach( function( rep ) {
var rez;
try {
var rez = JSON.parse( rep.raw );
rez = JSON.parse( rep.raw );
}
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) {
// 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 );
}
else {
@ -169,14 +206,15 @@ module.exports = function () {
var isValid = false;
var style = 'useful';
var errors = [];
var fmt = rez.meta &&
(rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars';
try {
var fmt = rez.meta && rez.meta.format === 'FRESH@0.1.0' ? 'fresh':'jars';
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 );
@ -191,15 +229,15 @@ module.exports = function () {
}
_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) {
_log( '--> '.bold.yellow + ( err.field.replace('data.','resume.').toUpperCase()
+ ' ' + err.message).yellow );
_log( '--> '.bold.yellow +
(err.field.replace('data.','resume.').toUpperCase() + ' ' +
err.message).yellow );
});
});
}
@ -221,22 +259,40 @@ module.exports = function () {
sheets.forEach(function(sheet, idx){
var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH';
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 );
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.
*/
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 ) {
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 );
});
}
@ -251,7 +307,8 @@ module.exports = function () {
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() },
{ 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,
validate: validate,
convert: convert,
new: create,
help: help
},
lib: require('./fluentlib'),
@ -284,5 +342,7 @@ module.exports = function () {
}();
}());
// [1]: JSON.parse throws SyntaxError on invalid JSON. See:
// 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'),
JsonGenerator: require('./gen/json-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.
*/
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;
info.theme.cssFiles.forEach( function( f ) {
var fi = PATH.parse( f[1].path );
FS.copySync( f[1].path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) {
var fi = PATH.parse( f.path );
FS.copySync( f.path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) {
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.
@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' )
, MD = require( 'marked' )
, XML = require( 'xml-escape' )
, PATH = require('path')
, MKDIRP = require('mkdirp')
, BaseGenerator = require( './base-generator' )
, EXTEND = require('../utils/extend')
, Theme = require('../core/theme');
// Default options.
var _defaultOpts = {
engine: 'underscore',
keepBreaks: true,
freezeBreaks: true,
freezeBreaks: false,
nSym: '&newl;', // newline entity
rSym: '&retn;', // return entity
template: {
@ -28,11 +36,11 @@ var _defaultOpts = {
out: function( txt ) { return txt; },
raw: function( txt ) { return txt; },
xml: function( txt ) { return XML(txt); },
md: function( txt ) { return MD(txt); },
mdin: function( txt ) { return MD(txt).replace(/^\s*\<p\>|\<\/p\>\s*$/gi, ''); },
md: function( txt ) { return MD( txt || '' ); },
mdin: function( txt ) { return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); },
lower: function( txt ) { return txt.toLowerCase(); },
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
indent_size: 2,
@ -42,38 +50,118 @@ var _defaultOpts = {
}
};
/**
TemplateGenerator performs resume generation via Underscore-style template
expansion and is appropriate for text-based formats like HTML, plain text,
and XML versions of Microsoft Word, Excel, and OpenOffice.
TemplateGenerator performs resume generation via local Handlebar or Underscore
style template expansion and is appropriate for text-based formats like HTML,
plain text, and XML versions of Microsoft Word, Excel, and OpenOffice.
@class TemplateGenerator
*/
var TemplateGenerator = module.exports = BaseGenerator.extend({
/** outputFormat: html, txt, pdf, doc
templateFormat: html or txt
**/
init: function( outputFormat, templateFormat, cssFile ){
this._super( 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 );
mk = this.single( rez, themeMarkup, this.format, cssInfo, { } );
this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f ));
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 ) {
// Carry over options
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
var tFolder = PATH.join(
PATH.parse( require.resolve('fluent-themes') ).dir,
@ -87,65 +175,35 @@ var TemplateGenerator = module.exports = BaseGenerator.extend({
}
}
// Load the theme
var theme = opts.themeObj || new Theme().open( tFolder );
var t = this.opts.themeObj || new Theme().open( tFolder );
// Load theme and CSS data
var tplFolder = PATH.join( tFolder, 'templates' );
var curFmt = theme.getFormat( this.format );
var cssInfo = { file: curFmt.css ? curFmt.cssPath : null, data: curFmt.css || null };
// Load the theme and format
return {
theme: t,
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 } ));
FS.writeFileSync( f, 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] + ' )) %}';
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' } );
}
else {
return '{% print( filt.out(' + p1 + ') ) %}';
catch(ex) {
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.
@ -156,6 +214,8 @@ function freeze( markup ) {
.replace( _reg.regR, _defaultOpts.rSym );
}
/**
Unfreeze newlines when the coast is clear.
*/
@ -165,6 +225,8 @@ function unfreeze( markup ) {
.replace( _reg.regSymN, '\n' );
}
/**
Regexes for linebreak preservation.
*/
@ -174,3 +236,7 @@ var _reg = {
regSymN: new RegExp( _defaultOpts.nSym, 'g' ),
regSymR: new RegExp( _defaultOpts.rSym, 'g' )
};
}());

View File

@ -59,17 +59,14 @@ function main() {
var splitAt = _.indexOf( params, 'to' );
if( splitAt === a._.length - 1 ) {
// 'TO' cannot be the last argument
logMsg('Please '.warn + 'specify an output file'.warnBold +
' for this operation or '.warn + 'omit the TO keyword'.warnBold + '.'.warn );
logMsg('Please '.warn + 'specify an output file'.warn.bold +
' for this operation or '.warn + 'omit the TO keyword'.warn.bold +
'.'.warn );
return;
}
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
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 ];
// Invoke the action
@ -82,10 +79,11 @@ function logMsg( msg ) {
}
function getOpts( args ) {
var noPretty = args['nopretty'] || args.n;
var noPretty = args.nopretty || args.n;
noPretty = noPretty && (noPretty === true || noPretty === 'true');
return {
theme: args.t || 'modern',
format: args.f || 'FRESH',
prettify: !noPretty,
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 4: msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide +
Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? 'or '.guide : '')
+ v.toUpperCase().guide;
}).join(', '.guide) + ") to get started.\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold;
return (idx === ar.length - 1 ? 'or '.guide : '') +
v.toUpperCase().guide;
}).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold;
break;
//case 4: msg = title + '\n' + ; break;
case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created 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 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break;
};
}
exitCode = ex.fluenterror;
}

View File

@ -1,22 +1,25 @@
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>
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:
COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a
custom theme.
COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a custom
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 NEW resume.json
fluentcv CONVERT resume.json TO resume-jrs.json
fluentcv VALIDATE resume.json
Both SOURCES and TARGETS can accept multiple files:
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
See https://github.com/fluentdesk/fluentCV/blob/master/README.md
for more information.
See https://github.com/fluentdesk/fluentCV/blob/master/README.md for more
information.

View File

@ -41,7 +41,7 @@
return ret;
};
})(name, prop[name]) :
})(name, prop[name]) : // jshint ignore:line
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);
// });
// });