mirror of
https://github.com/JuanCanham/HackMyResume.git
synced 2025-05-12 00:27:08 +01:00
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
78a8b9c58e | |||
5e7abb66bd | |||
358c397bb9 | |||
3d41528059 | |||
79637b611a | |||
5de796b119 | |||
bf84341acf | |||
bbac1fdceb | |||
c5ee1ee33c | |||
c74eda90ed | |||
ef2fe95bd8 | |||
e2589b3730 | |||
ebad1677bc | |||
dab6ebfd82 | |||
dd61b5360a | |||
fced92a5a0 | |||
64db1a654e | |||
31830ee759 | |||
1c05846a4f | |||
1db9c2e420 | |||
c966f6766c | |||
65b6359fd8 | |||
a54476eede | |||
7c0354046c | |||
43cd1c7e52 | |||
f80c333361 | |||
cdbb264093 | |||
87b3bbe785 | |||
b92cf7298a | |||
93456b5f40 | |||
72f29bf402 | |||
f6fc384466 | |||
c5ab3fdfae | |||
78c5081a29 |
54
README.md
54
README.md
@ -2,7 +2,8 @@ HackMyResume
|
||||
============
|
||||
*Create polished résumés and CVs in multiple formats from your command line or
|
||||
shell. Author in clean Markdown and JSON, export to Word, HTML, PDF, LaTeX,
|
||||
plain text, and other arbitrary formats.*
|
||||
plain text, and other arbitrary formats. Fight the power, save trees. Compatible
|
||||
with [FRESH][fresca] and [JRS][6] resumes.*
|
||||
|
||||

|
||||
|
||||
@ -24,8 +25,6 @@ or 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].
|
||||
@ -49,8 +48,9 @@ To use HackMyResume 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 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.
|
||||
- **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.
|
||||
|
||||
```bash
|
||||
# hackmyresume BUILD <INPUTS> TO <OUTPUTS> [-t THEME]
|
||||
@ -58,7 +58,7 @@ it when you need to submit, upload, print, or email resumes in specific formats.
|
||||
hackmyresume 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.
|
||||
- **new** creates a new resume in FRESH or JSON Resume format.
|
||||
|
||||
```bash
|
||||
# hackmyresume NEW <OUTPUTS> [-f <FORMAT>]
|
||||
@ -67,8 +67,10 @@ it when you need to submit, upload, print, or email resumes in specific formats.
|
||||
hackmyresume 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.
|
||||
- **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.
|
||||
|
||||
```bash
|
||||
# hackmyresume CONVERT <INPUTS> TO <OUTPUTS>
|
||||
@ -76,7 +78,7 @@ Use it to convert between the two formats to take advantage of tools and service
|
||||
hackmyresume CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json
|
||||
```
|
||||
|
||||
- `validate` validates the specified resume against either the FRESH or JSON
|
||||
- **validate** validates the specified resume against either the FRESH or JSON
|
||||
Resume schema. Use it to make sure your resume data is sufficient and complete.
|
||||
|
||||
```bash
|
||||
@ -161,25 +163,34 @@ Generating YAML resume: out/resume.yml
|
||||
|
||||
### Applying a theme
|
||||
|
||||
You can specify a predefined or custom theme via the optional `-t` parameter. For a predefined theme, include the theme name. For a custom theme, include the path to the custom theme's folder.
|
||||
You can specify a predefined or custom theme via the optional `-t` parameter.
|
||||
For a predefined theme, include the theme name. For a custom theme, include the
|
||||
path to the custom theme's folder.
|
||||
|
||||
```bash
|
||||
hackmyresume build resume.json -t modern
|
||||
hackmyresume build resume.json -t ~/foo/bar/my-custom-theme/
|
||||
```
|
||||
|
||||
As of v0.9.0, available predefined themes are `modern`, `minimist`, and `hello-world`, and `compact`.
|
||||
As of v1.0.0, available predefined themes are `positive`, `modern`, `compact`,
|
||||
`minimist`, and `hello-world`.
|
||||
|
||||
### Merging resumes
|
||||
|
||||
You can **merge multiple resumes together** by specifying them in order from most generic to most specific:
|
||||
You can **merge multiple resumes together** by specifying them in order from
|
||||
most generic to most specific:
|
||||
|
||||
```bash
|
||||
# Merge specific.json onto base.json and generate all formats
|
||||
hackmyresume build base.json specific.json -o resume.all
|
||||
```
|
||||
|
||||
This can be useful for overriding a base (generic) resume with information from a specific (targeted) resume. For example, you might override your generic catch-all "software developer" resume with specific details from your targeted "game developer" resume, or combine two partial resumes into a "complete" resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge:
|
||||
This can be useful for overriding a base (generic) resume with information from
|
||||
a specific (targeted) resume. For example, you might override your generic
|
||||
catch-all "software developer" resume with specific details from your targeted
|
||||
"game developer" resume, or combine two partial resumes into a "complete"
|
||||
resume. Merging follows conventional [extend()][9]-style behavior and there's
|
||||
no arbitrary limit to how many resumes you can merge:
|
||||
|
||||
```bash
|
||||
hackmyresume build in1.json in2.json in3.json in4.json TO out.html out.doc
|
||||
@ -210,14 +221,17 @@ hackmyresume build resume.json
|
||||
|
||||
### Using .all
|
||||
|
||||
The special `.all` extension tells HackMyResume to generate all supported output formats for the given resume. For example, this...
|
||||
The special `.all` extension tells HackMyResume to generate all supported output
|
||||
formats for the given resume. For example, this...
|
||||
|
||||
```bash
|
||||
# Generate all resume formats (HTML, PDF, DOC, TXT, etc.)
|
||||
hackmyresume build me.json -o out/resume.all
|
||||
```
|
||||
|
||||
..tells HackMyResume to read `me.json` and generate `out/resume.md`, `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`.
|
||||
..tells HackMyResume to read `me.json` and generate `out/resume.md`,
|
||||
`out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and
|
||||
`out/resume.json`.
|
||||
|
||||
### Validating
|
||||
|
||||
@ -240,17 +254,17 @@ Validating JSON resume: resumeB.json (VALID)
|
||||
|
||||
### Converting
|
||||
|
||||
HackMyResume can convert between the [FRESH][fresca] and [JSON Resume][6] formats.
|
||||
Just run:
|
||||
HackMyResume can convert between the [FRESH][fresca] and [JSON Resume][6]
|
||||
formats. Just run:
|
||||
|
||||
```bash
|
||||
hackmyresume CONVERT <INPUTS> <OUTPUTS>
|
||||
```
|
||||
|
||||
where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and
|
||||
<OUTPUTS> is a corresponding list of output file names. HackMyResume will autodetect
|
||||
the format (FRESH or JRS) of each input resume and convert it to the other
|
||||
format (JRS or FRESH).
|
||||
<OUTPUTS> is a corresponding list of output file names. HackMyResume will
|
||||
autodetect the format (FRESH or JRS) of each input resume and convert it to the
|
||||
other format (JRS or FRESH).
|
||||
|
||||
### Prettifying
|
||||
|
||||
|
15
package.json
15
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hackmyresume",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.2",
|
||||
"description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -18,9 +18,13 @@
|
||||
"PDF",
|
||||
"YAML",
|
||||
"HTML",
|
||||
"CLI"
|
||||
"LaTeX",
|
||||
"CLI",
|
||||
"Handlebars",
|
||||
"Underscore",
|
||||
"template"
|
||||
],
|
||||
"author": "hacksalot <hacksalot@fluentdesk.com> (https://github.com/hacksalot)",
|
||||
"author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
|
||||
"license": "MIT",
|
||||
"preferGlobal": "true",
|
||||
"bugs": {
|
||||
@ -33,17 +37,20 @@
|
||||
"homepage": "https://github.com/hacksalot/HackMyResume",
|
||||
"dependencies": {
|
||||
"colors": "^1.1.2",
|
||||
"fluent-themes": "~0.7.0-beta",
|
||||
"fluent-themes": "~0.7.1-beta",
|
||||
"fresca": "~0.2.2",
|
||||
"fs-extra": "^0.24.0",
|
||||
"handlebars": "^4.0.5",
|
||||
"html": "0.0.10",
|
||||
"is-my-json-valid": "^2.12.2",
|
||||
"jst": "0.0.13",
|
||||
"lodash": "^3.10.1",
|
||||
"marked": "^0.3.5",
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.10.6",
|
||||
"parse-filepath": "^0.6.3",
|
||||
"path-exists": "^2.1.0",
|
||||
"recursive-readdir-sync": "^1.0.6",
|
||||
"simple-html-tokenizer": "^0.2.0",
|
||||
"underscore": "^1.8.3",
|
||||
|
@ -16,6 +16,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
/**
|
||||
Convert from JSON Resume format to FRESH.
|
||||
@method toFresh
|
||||
@todo Refactor
|
||||
*/
|
||||
toFRESH: function( src, foreign ) {
|
||||
|
||||
@ -67,6 +68,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
Convert from FRESH format to JSON Resume.
|
||||
@param foreign True if non-JSON-Resume properties should be included in
|
||||
the result, false if those properties should be excluded.
|
||||
@todo Refactor
|
||||
*/
|
||||
toJRS: function( src, foreign ) {
|
||||
|
||||
@ -112,6 +114,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
};
|
||||
|
||||
function meta( direction, obj ) {
|
||||
if( !obj ) return obj; // preserve null and undefined
|
||||
if( direction ) {
|
||||
obj = obj || { };
|
||||
obj.format = obj.format || "FRESH@0.1.0";
|
||||
@ -121,6 +124,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
|
||||
function employment( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( !direction ) {
|
||||
return obj && obj.history ?
|
||||
obj.history.map(function(emp){
|
||||
@ -157,6 +161,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
|
||||
|
||||
function education( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
return obj && obj.length ? {
|
||||
history: obj.map(function(edu){
|
||||
@ -191,6 +196,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
|
||||
function service( obj, direction, foreign ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
return {
|
||||
history: obj && obj.length ? obj.map(function(vol) {
|
||||
@ -225,6 +231,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
|
||||
function social( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
return obj.map(function(pro){
|
||||
return {
|
||||
@ -247,6 +254,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
|
||||
function recognition( obj, direction, foreign ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
return obj && obj.length ? obj.map(
|
||||
function(awd){
|
||||
@ -275,6 +283,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
|
||||
function references( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
return obj && obj.length && obj.map(function(ref){
|
||||
return {
|
||||
@ -296,6 +305,7 @@ FRESH to JSON Resume conversion routiens.
|
||||
}
|
||||
|
||||
function writing( obj, direction ) {
|
||||
if( !obj ) return obj;
|
||||
if( direction ) {
|
||||
return obj.map(function( pub ) {
|
||||
return {
|
||||
|
19
src/core/default-formats.js
Normal file
19
src/core/default-formats.js
Normal file
@ -0,0 +1,19 @@
|
||||
(function(){
|
||||
|
||||
var FLUENT = require('../hackmyapi');
|
||||
|
||||
/**
|
||||
Supported resume formats.
|
||||
*/
|
||||
module.exports = [
|
||||
{ name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() },
|
||||
{ name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() },
|
||||
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() },
|
||||
{ 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: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() }
|
||||
];
|
||||
|
||||
}());
|
13
src/core/default-options.js
Normal file
13
src/core/default-options.js
Normal file
@ -0,0 +1,13 @@
|
||||
(function(){
|
||||
|
||||
module.exports = {
|
||||
theme: 'modern',
|
||||
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
|
||||
indent_size: 2,
|
||||
unformatted: ['em','strong'],
|
||||
max_char: 80, // ← See lib/html.js in above-linked repo
|
||||
//wrap_line_length: 120, ← Don't use this
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
@ -10,6 +10,7 @@ Definition of the FRESHResume class.
|
||||
, extend = require('../utils/extend')
|
||||
, validator = require('is-my-json-valid')
|
||||
, _ = require('underscore')
|
||||
, __ = require('lodash')
|
||||
, PATH = require('path')
|
||||
, moment = require('moment')
|
||||
, MD = require('marked')
|
||||
@ -187,15 +188,6 @@ Definition of the FRESHResume class.
|
||||
return flatSkills;
|
||||
},
|
||||
|
||||
/**
|
||||
Update the sheet's raw data. TODO: remove/refactor
|
||||
*/
|
||||
FreshResume.prototype.updateData = function( str ) {
|
||||
this.clear( false );
|
||||
this.parse( str );
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
Reset the sheet to an empty state.
|
||||
*/
|
||||
@ -299,7 +291,7 @@ Definition of the FRESHResume class.
|
||||
Validate the sheet against the FRESH Resume schema.
|
||||
*/
|
||||
FreshResume.prototype.isValid = function( info ) {
|
||||
var schemaObj = require('FRESCA');
|
||||
var schemaObj = require('fresca');
|
||||
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})?)?$/ }
|
||||
@ -321,7 +313,8 @@ Definition of the FRESHResume class.
|
||||
sheets that have overlapping jobs.
|
||||
*/
|
||||
FreshResume.prototype.duration = function() {
|
||||
if( this.employment.history && this.employment.history.length ) {
|
||||
var empHist = __.get(this, 'employment.history');
|
||||
if( empHist && empHist.length ) {
|
||||
var firstJob = _.last( this.employment.history );
|
||||
var careerStart = firstJob.start ? firstJob.safe.start : '';
|
||||
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
|
||||
@ -341,9 +334,9 @@ Definition of the FRESHResume class.
|
||||
*/
|
||||
FreshResume.prototype.sort = function( ) {
|
||||
|
||||
this.employment.history && this.employment.history.sort( byDateDesc );
|
||||
this.education.history && this.education.history.sort( byDateDesc );
|
||||
this.service.history && this.service.history.sort( byDateDesc );
|
||||
__.get(this, 'employment.history') && this.employment.history.sort( byDateDesc );
|
||||
__.get(this, 'education.history') && this.education.history.sort( byDateDesc );
|
||||
__.get(this, 'service.history') && this.service.history.sort( byDateDesc );
|
||||
|
||||
// this.awards && this.awards.sort( function(a, b) {
|
||||
// return( a.safeDate.isBefore(b.safeDate) ) ? 1
|
||||
|
@ -107,15 +107,6 @@ 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 );
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
Reset the sheet to an empty state.
|
||||
*/
|
||||
|
13
src/core/load-source-resumes.js
Normal file
13
src/core/load-source-resumes.js
Normal file
@ -0,0 +1,13 @@
|
||||
(function(){
|
||||
|
||||
var FRESHResume = require('../core/fresh-resume');
|
||||
|
||||
module.exports = function loadSourceResumes( src, log, fn ) {
|
||||
return src.map( function( res ) {
|
||||
log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info +
|
||||
res.cyan.bold );
|
||||
return (fn && fn(res)) || (new FRESHResume()).open( res );
|
||||
});
|
||||
};
|
||||
|
||||
}());
|
@ -11,6 +11,7 @@ Definition of the Theme class.
|
||||
, validator = require('is-my-json-valid')
|
||||
, _ = require('underscore')
|
||||
, PATH = require('path')
|
||||
, parsePath = require('parse-filepath')
|
||||
, EXTEND = require('../utils/extend')
|
||||
, moment = require('moment')
|
||||
, RECURSIVE_READ_DIR = require('recursive-readdir-sync');
|
||||
@ -30,8 +31,8 @@ Definition of the Theme class.
|
||||
|
||||
// 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 pathInfo = parsePath( themeFolder );
|
||||
var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' );
|
||||
var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) );
|
||||
var that = this;
|
||||
|
||||
@ -59,7 +60,7 @@ Definition of the Theme class.
|
||||
this.formats = formatsHash;
|
||||
|
||||
// Set the official theme name
|
||||
this.name = PATH.parse( this.folder ).name;
|
||||
this.name = parsePath( this.folder ).name;
|
||||
|
||||
return this;
|
||||
};
|
||||
@ -96,9 +97,9 @@ Definition of the Theme class.
|
||||
// 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 pathInfo = parsePath(absPath);
|
||||
var outFmt = '', isMajor = false;
|
||||
var portion = pathInfo.dir.replace(tplFolder,'');
|
||||
var portion = pathInfo.dirname.replace(tplFolder,'');
|
||||
if( portion && portion.trim() ) {
|
||||
if( portion[1] === '_' ) return;
|
||||
var reg = /^(?:\/|\\)(html|latex|doc|pdf|partials)(?:\/|\\)?/ig;
|
||||
@ -134,8 +135,8 @@ Definition of the Theme class.
|
||||
action: 'transform',
|
||||
path: absPath,
|
||||
major: isMajor,
|
||||
orgPath: PATH.relative(that.folder, absPath),
|
||||
ext: pathInfo.ext.slice(1),
|
||||
orgPath: PATH.relative(tplFolder, absPath),
|
||||
ext: pathInfo.extname.slice(1),
|
||||
title: friendlyName( outFmt ),
|
||||
pre: outFmt,
|
||||
// outFormat: outFmt || pathInfo.name,
|
||||
@ -186,7 +187,7 @@ Definition of the Theme class.
|
||||
|
||||
act = null;
|
||||
// If this file is mentioned in the theme's JSON file under "transforms"
|
||||
var pathInfo = PATH.parse(absPath);
|
||||
var pathInfo = parsePath(absPath);
|
||||
var absPathSafe = absPath.trim().toLowerCase();
|
||||
var outFmt = _.find( Object.keys( that.formats ), function( fmtKey ) {
|
||||
var fmtVal = that.formats[ fmtKey ];
|
||||
@ -203,7 +204,7 @@ Definition of the Theme class.
|
||||
// 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,'');
|
||||
var portion = pathInfo.dirname.replace(tplFolder,'');
|
||||
if( portion && portion.trim() ) {
|
||||
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
|
||||
var res = reg.exec( portion );
|
||||
@ -231,7 +232,7 @@ Definition of the Theme class.
|
||||
action: act,
|
||||
orgPath: PATH.relative(that.folder, absPath),
|
||||
path: absPath,
|
||||
ext: pathInfo.ext.slice(1),
|
||||
ext: pathInfo.extname.slice(1),
|
||||
title: friendlyName( outFmt ),
|
||||
pre: outFmt,
|
||||
// outFormat: outFmt || pathInfo.name,
|
||||
|
169
src/eng/generic-helpers.js
Normal file
169
src/eng/generic-helpers.js
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
Generic template helper definitions for FluentCV.
|
||||
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
|
||||
@module generic-helpers.js
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
var MD = require('marked')
|
||||
, H2W = require('../utils/html-to-wpml')
|
||||
, moment = require('moment')
|
||||
, _ = require('underscore');
|
||||
|
||||
/**
|
||||
Generic template helper function definitions.
|
||||
@class GenericHelpers
|
||||
*/
|
||||
var GenericHelpers = module.exports = {
|
||||
|
||||
/**
|
||||
Convert the input date to a specified format through Moment.js.
|
||||
@method formatDate
|
||||
*/
|
||||
formatDate: function(datetime, format) {
|
||||
return moment ? moment( datetime ).format( format ) : datetime;
|
||||
},
|
||||
|
||||
/**
|
||||
Convert inline Markdown to inline WordProcessingML.
|
||||
@method wpml
|
||||
*/
|
||||
wpml: function( txt, inline ) {
|
||||
if(!txt) return '';
|
||||
inline = (inline && !inline.hash) || false;
|
||||
txt = inline ?
|
||||
MD(txt.trim()).replace(/^\s*<p>|<\/p>\s*$/gi, '') :
|
||||
MD(txt.trim());
|
||||
txt = H2W( txt.trim() );
|
||||
return txt;
|
||||
},
|
||||
|
||||
/**
|
||||
Emit a conditional link.
|
||||
@method link
|
||||
*/
|
||||
link: function( text, url ) {
|
||||
return url && url.trim() ?
|
||||
('<a href="' + url + '">' + text + '</a>') : text;
|
||||
},
|
||||
|
||||
/**
|
||||
Return the last word of the specified text.
|
||||
@method lastWord
|
||||
*/
|
||||
lastWord: function( txt ) {
|
||||
return txt && txt.trim() ? _.last( txt.split(' ') ) : '';
|
||||
},
|
||||
|
||||
/**
|
||||
Convert a skill level to an RGB color triplet.
|
||||
@method skillColor
|
||||
@param lvl Input skill level. Skill level can be expressed as a string
|
||||
("beginner", "intermediate", etc.), as an integer (1,5,etc), as a string
|
||||
integer ("1", "5", etc.), or as an RRGGBB color triplet ('#C00000',
|
||||
'#FFFFAA').
|
||||
*/
|
||||
skillColor: function( lvl ) {
|
||||
var idx = skillLevelToIndex( lvl );
|
||||
var skillColors = (this.theme && this.theme.palette &&
|
||||
this.theme.palette.skillLevels) ||
|
||||
[ '#FFFFFF', '#5CB85C', '#F1C40F', '#428BCA', '#C00000' ];
|
||||
return skillColors[idx];
|
||||
},
|
||||
|
||||
/**
|
||||
Return an appropriate height.
|
||||
@method lastWord
|
||||
*/
|
||||
skillHeight: function( lvl ) {
|
||||
var idx = skillLevelToIndex( lvl );
|
||||
return ['38.25', '30', '16', '8', '0'][idx];
|
||||
},
|
||||
|
||||
/**
|
||||
Return all but the last word of the input text.
|
||||
@method initialWords
|
||||
*/
|
||||
initialWords: function( txt ) {
|
||||
return txt && txt.trim() ? _.initial( txt.split(' ') ).join(' ') : '';
|
||||
},
|
||||
|
||||
/**
|
||||
Trim the protocol (http or https) from a URL/
|
||||
@method trimURL
|
||||
*/
|
||||
trimURL: function( url ) {
|
||||
return url && url.trim() ? url.trim().replace(/^https?:\/\//i, '') : '';
|
||||
},
|
||||
|
||||
/**
|
||||
Convert text to lowercase.
|
||||
@method toLower
|
||||
*/
|
||||
toLower: function( txt ) {
|
||||
return txt && txt.trim() ? txt.toLowerCase() : '';
|
||||
},
|
||||
|
||||
/**
|
||||
Return true if either value is truthy.
|
||||
@method either
|
||||
*/
|
||||
either: function( lhs, rhs, options ) {
|
||||
if (lhs || rhs) return options.fn(this);
|
||||
},
|
||||
|
||||
/**
|
||||
Perform a generic comparison.
|
||||
See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates
|
||||
@method compare
|
||||
*/
|
||||
compare: function(lvalue, rvalue, options) {
|
||||
if (arguments.length < 3)
|
||||
throw new Error("Handlerbars Helper 'compare' needs 2 parameters");
|
||||
var operator = options.hash.operator || "==";
|
||||
var operators = {
|
||||
'==': function(l,r) { return l == r; },
|
||||
'===': function(l,r) { return l === r; },
|
||||
'!=': function(l,r) { return l != r; },
|
||||
'<': function(l,r) { return l < r; },
|
||||
'>': function(l,r) { return l > r; },
|
||||
'<=': function(l,r) { return l <= r; },
|
||||
'>=': function(l,r) { return l >= r; },
|
||||
'typeof': function(l,r) { return typeof l == r; }
|
||||
};
|
||||
if (!operators[operator])
|
||||
throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator);
|
||||
var result = operators[operator](lvalue,rvalue);
|
||||
return result ? options.fn(this) : options.inverse(this);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function skillLevelToIndex( lvl ) {
|
||||
var idx = 0;
|
||||
if( String.is( lvl ) ) {
|
||||
lvl = lvl.trim().toLowerCase();
|
||||
var intVal = parseInt( lvl );
|
||||
if( isNaN( intVal ) ) {
|
||||
switch( lvl ) {
|
||||
case 'beginner': idx = 1; break;
|
||||
case 'intermediate': idx = 2; break;
|
||||
case 'advanced': idx = 3; break;
|
||||
case 'master': idx = 4; break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
idx = Math.min( intVal / 2, 4 );
|
||||
idx = Math.max( 0, idx );
|
||||
}
|
||||
}
|
||||
else {
|
||||
idx = Math.min( lvl / 2, 4 );
|
||||
idx = Math.max( 0, idx );
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
}());
|
@ -31,7 +31,7 @@ Definition of the HandlebarsGenerator class.
|
||||
});
|
||||
|
||||
// Register necessary helpers.
|
||||
registerHelpers();
|
||||
registerHelpers( theme );
|
||||
|
||||
// Compile and run the Handlebars template.
|
||||
var template = HANDLEBARS.compile(jst);
|
||||
|
@ -8,116 +8,17 @@ Template helper definitions for Handlebars.
|
||||
(function() {
|
||||
|
||||
var HANDLEBARS = require('handlebars')
|
||||
, MD = require('marked')
|
||||
, H2W = require('../utils/html-to-wpml')
|
||||
, moment = require('moment')
|
||||
, _ = require('underscore');
|
||||
, _ = require('underscore')
|
||||
, helpers = require('./generic-helpers');
|
||||
|
||||
/**
|
||||
Register useful Handlebars helpers.
|
||||
@method registerHelpers
|
||||
*/
|
||||
module.exports = function() {
|
||||
module.exports = function( theme ) {
|
||||
|
||||
// Set up a date formatting helper so we can do:
|
||||
// {{formatDate val 'YYYY-MM'}}
|
||||
HANDLEBARS.registerHelper("formatDate", function(datetime, format) {
|
||||
return moment ? moment( datetime ).format( format ) : datetime;
|
||||
});
|
||||
|
||||
// Set up a Markdown-to-WordProcessingML helper so we can do:
|
||||
// {{wmpl val [true|false]}}
|
||||
HANDLEBARS.registerHelper("wpml", function( txt, inline ) {
|
||||
if(!txt) return '';
|
||||
inline = (inline && !inline.hash) || false;
|
||||
txt = inline ?
|
||||
MD(txt.trim()).replace(/^\s*<p>|<\/p>\s*$/gi, '') :
|
||||
MD(txt.trim());
|
||||
txt = H2W( txt.trim() );
|
||||
return txt;
|
||||
});
|
||||
|
||||
// Set up a last-word helper so we can do:
|
||||
// {{lastWord val [true|false]}}
|
||||
HANDLEBARS.registerHelper("link", function( text, url ) {
|
||||
return url && url.trim() ?
|
||||
('<a href="' + url + '">' + text + '</a>') : text;
|
||||
});
|
||||
|
||||
// Set up a last-word helper so we can do:
|
||||
// {{lastWord val [true|false]}}
|
||||
HANDLEBARS.registerHelper("lastWord", function( txt ) {
|
||||
return txt && txt.trim() ? _.last( txt.split(' ') ) : '';
|
||||
});
|
||||
|
||||
// Set up a skill colorizing helper:
|
||||
// {{skillColor val}}
|
||||
HANDLEBARS.registerHelper("skillColor", function( lvl ) {
|
||||
switch(lvl) {
|
||||
case 'beginner': return '#5CB85C';
|
||||
case 'intermediate': return '#F1C40F';
|
||||
case 'advanced': return '#428BCA';
|
||||
case 'master': return '#C00000';
|
||||
}
|
||||
});
|
||||
|
||||
// Set up a skill colorizing helper:
|
||||
// {{skillColor val}}
|
||||
HANDLEBARS.registerHelper("skillHeight", function( lvl ) {
|
||||
switch(lvl) {
|
||||
case 'beginner': return '30';
|
||||
case 'intermediate': return '16';
|
||||
case 'advanced': return '8';
|
||||
case 'master': return '0';
|
||||
}
|
||||
});
|
||||
|
||||
// Set up a Markdown-to-WordProcessingML helper so we can do:
|
||||
// {{initialWords val [true|false]}}
|
||||
HANDLEBARS.registerHelper("initialWords", function( txt ) {
|
||||
return txt && txt.trim() ? _.initial( txt.split(' ') ).join(' ') : '';
|
||||
});
|
||||
|
||||
// Set up a URL-trimming helper to drop the protocol so we can do:
|
||||
// {{trimURL url}}
|
||||
HANDLEBARS.registerHelper("trimURL", function( url ) {
|
||||
return url && url.trim() ? url.trim().replace(/^https?:\/\//i, '') : '';
|
||||
});
|
||||
|
||||
// Set up a URL-trimming helper to drop the protocol so we can do:
|
||||
// {{trimURL url}}
|
||||
HANDLEBARS.registerHelper("toLower", function( txt ) {
|
||||
return txt && txt.trim() ? txt.toLowerCase() : '';
|
||||
});
|
||||
|
||||
// Set up a Markdown-to-WordProcessingML helper so we can do:
|
||||
// {{either A B}}
|
||||
HANDLEBARS.registerHelper("either", function( lhs, rhs, options ) {
|
||||
if (lhs || rhs) return options.fn(this);
|
||||
});
|
||||
|
||||
// Set up a generic conditional helper so we can do:
|
||||
// {{compare val otherVal operator="<"}}
|
||||
// http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates/
|
||||
HANDLEBARS.registerHelper('compare', function(lvalue, rvalue, options) {
|
||||
if (arguments.length < 3)
|
||||
throw new Error("Handlerbars Helper 'compare' needs 2 parameters");
|
||||
var operator = options.hash.operator || "==";
|
||||
var operators = {
|
||||
'==': function(l,r) { return l == r; },
|
||||
'===': function(l,r) { return l === r; },
|
||||
'!=': function(l,r) { return l != r; },
|
||||
'<': function(l,r) { return l < r; },
|
||||
'>': function(l,r) { return l > r; },
|
||||
'<=': function(l,r) { return l <= r; },
|
||||
'>=': function(l,r) { return l >= r; },
|
||||
'typeof': function(l,r) { return typeof l == r; }
|
||||
};
|
||||
if (!operators[operator])
|
||||
throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator);
|
||||
var result = operators[operator](lvalue,rvalue);
|
||||
return result ? options.fn(this) : options.inverse(this);
|
||||
});
|
||||
helpers.theme = theme;
|
||||
HANDLEBARS.registerHelper( helpers );
|
||||
|
||||
};
|
||||
|
||||
|
@ -22,16 +22,6 @@ Definition of the HTMLGenerator class.
|
||||
the HTML resume prior to saving.
|
||||
*/
|
||||
onBeforeSave: function( info ) {
|
||||
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.path );
|
||||
FS.copySync( f.path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) {
|
||||
throw { fluenterror: that.codes.copyCss, data: [cssSrc,cssDst] };
|
||||
});
|
||||
});
|
||||
|
||||
return this.opts.prettify ?
|
||||
HTML.prettyPrint( info.mk, this.opts.prettify ) : info.mk;
|
||||
}
|
||||
|
@ -23,8 +23,8 @@ Definition of the HtmlPdfGenerator class.
|
||||
Generate the binary PDF.
|
||||
*/
|
||||
onBeforeSave: function( info ) {
|
||||
pdf(info.mk, info.outputFile);
|
||||
return info.mk;
|
||||
pdf( info.mk, info.outputFile );
|
||||
return null; // halt further processing
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -13,6 +13,7 @@ Definition of the TemplateGenerator class.
|
||||
, MD = require( 'marked' )
|
||||
, XML = require( 'xml-escape' )
|
||||
, PATH = require('path')
|
||||
, parsePath = require('parse-filepath')
|
||||
, MKDIRP = require('mkdirp')
|
||||
, BaseGenerator = require( './base-generator' )
|
||||
, EXTEND = require('../utils/extend')
|
||||
@ -38,7 +39,9 @@ Definition of the TemplateGenerator class.
|
||||
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, ''); },
|
||||
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; }
|
||||
@ -69,24 +72,14 @@ Definition of the TemplateGenerator class.
|
||||
},
|
||||
|
||||
|
||||
|
||||
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.
|
||||
@method generate
|
||||
String-based template generation method.
|
||||
@method invoke
|
||||
@param rez A FreshResume object.
|
||||
@param f Full path to the output resume file to generate.
|
||||
@param opts Generator options.
|
||||
@returns An array of strings representing generated output files.
|
||||
*/
|
||||
generate: function( rez, f, opts ) {
|
||||
invoke: function( rez, opts ) {
|
||||
|
||||
// Carry over options
|
||||
this.opts = EXTEND( true, { }, _defaultOpts, opts );
|
||||
@ -96,20 +89,81 @@ Definition of the TemplateGenerator class.
|
||||
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 );
|
||||
return {
|
||||
files: curFmt.files.map( function( tplInfo ) {
|
||||
return {
|
||||
info: tplInfo,
|
||||
data: tplInfo.action === 'transform' ?
|
||||
transform.call( that, rez, tplInfo, theme ) : undefined
|
||||
};
|
||||
}).filter(function(item){ return item !== null; }),
|
||||
themeInfo: themeInfo
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
File-based template generation method.
|
||||
@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 ) {
|
||||
|
||||
// Call the generation method
|
||||
var genInfo = this.invoke( rez, opts );
|
||||
|
||||
// Carry over options
|
||||
this.opts = EXTEND( true, { }, _defaultOpts, opts );
|
||||
|
||||
// Load the theme
|
||||
var themeInfo = genInfo.themeInfo;
|
||||
var theme = themeInfo.theme;
|
||||
var tFolder = themeInfo.folder;
|
||||
var tplFolder = PATH.join( tFolder, 'src' );
|
||||
var outFolder = parsePath(f).dirname;
|
||||
var curFmt = theme.getFormat( this.format );
|
||||
var that = this;
|
||||
|
||||
// "Generate": process individual files within the theme
|
||||
genInfo.files.forEach(function( file ){
|
||||
|
||||
var thisFilePath;
|
||||
|
||||
if( file.info.action === 'transform' ) {
|
||||
thisFilePath = PATH.join( outFolder, file.info.orgPath );
|
||||
try {
|
||||
if( that.onBeforeSave ) {
|
||||
file.data = that.onBeforeSave({
|
||||
theme: theme,
|
||||
outputFile: (file.info.major ? f : thisFilePath),
|
||||
mk: file.data
|
||||
});
|
||||
if( !file.data ) return; // PDF etc
|
||||
}
|
||||
var fileName = file.info.major ? f : thisFilePath;
|
||||
MKDIRP.sync( PATH.dirname( fileName ) );
|
||||
FS.writeFileSync( fileName, file.data,
|
||||
{ encoding: 'utf8', flags: 'w' } );
|
||||
that.onAfterSave && that.onAfterSave(
|
||||
{ outputFile: fileName, mk: file.data } );
|
||||
}
|
||||
catch(ex) {
|
||||
console.log(ex);
|
||||
}
|
||||
}
|
||||
else if( tplInfo.action === null && theme.explicit ) {
|
||||
var thisFilePath = PATH.join(outFolder, tplInfo.orgPath);
|
||||
else if( file.info.action === null/* && theme.explicit*/ ) {
|
||||
thisFilePath = PATH.join( outFolder, file.info.orgPath );
|
||||
try {
|
||||
MKDIRP.sync( PATH.dirname(thisFilePath) );
|
||||
FS.copySync( tplInfo.path, thisFilePath );
|
||||
FS.copySync( file.info.path, thisFilePath );
|
||||
}
|
||||
catch(ex) {
|
||||
console.log(ex);
|
||||
@ -123,7 +177,7 @@ Definition of the TemplateGenerator class.
|
||||
var absLoc = PATH.join(outFolder, loc);
|
||||
var absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
|
||||
// 'file', 'dir', or 'junction' (Windows only)
|
||||
var type = PATH.parse( absLoc ).ext ? 'file' : 'junction';
|
||||
var type = parsePath( absLoc ).extname ? 'file' : 'junction';
|
||||
FS.symlinkSync( absTarg, absLoc, type);
|
||||
});
|
||||
}
|
||||
@ -142,8 +196,10 @@ Definition of the TemplateGenerator class.
|
||||
*/
|
||||
single: function( json, jst, format, cssInfo, opts, theme ) {
|
||||
this.opts.freezeBreaks && ( jst = freeze(jst) );
|
||||
|
||||
var eng = require( '../eng/' + theme.engine + '-generator' );
|
||||
var result = eng.generate( json, jst, format, cssInfo, opts, theme );
|
||||
|
||||
this.opts.freezeBreaks && ( result = unfreeze(result) );
|
||||
return result;
|
||||
}
|
||||
@ -166,10 +222,10 @@ Definition of the TemplateGenerator class.
|
||||
function themeFromMoniker() {
|
||||
// Verify the specified theme name/path
|
||||
var tFolder = PATH.join(
|
||||
PATH.parse( require.resolve('fluent-themes') ).dir,
|
||||
parsePath( require.resolve('fluent-themes') ).dirname,
|
||||
this.opts.theme
|
||||
);
|
||||
var exists = require('../utils/file-exists');
|
||||
var exists = require('path-exists').sync;
|
||||
if( !exists( tFolder ) ) {
|
||||
tFolder = PATH.resolve( this.opts.theme );
|
||||
if( !exists( tFolder ) ) {
|
||||
@ -188,18 +244,15 @@ Definition of the TemplateGenerator class.
|
||||
|
||||
|
||||
|
||||
/**
|
||||
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, theme );
|
||||
this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } ));
|
||||
var thisFilePath = PATH.join( outFolder, tplInfo.orgPath );
|
||||
function transform( rez, tplInfo, theme ) {
|
||||
try {
|
||||
MKDIRP.sync( PATH.dirname( tplInfo.major ? f : thisFilePath) );
|
||||
FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } );
|
||||
this.onAfterSave && (mk = this.onAfterSave( { outputFile: (tplInfo.major ? f : thisFilePath), mk: mk } ));
|
||||
var cssInfo = {
|
||||
file: tplInfo.css ? tplInfo.cssPath : null,
|
||||
data: tplInfo.css || null
|
||||
};
|
||||
|
||||
return this.single( rez, tplInfo.data, this.format, cssInfo, this.opts,
|
||||
theme );
|
||||
}
|
||||
catch(ex) {
|
||||
console.log(ex);
|
||||
|
323
src/hackmycmd.js
323
src/hackmycmd.js
@ -7,279 +7,9 @@ Internal resume generation logic for HackMyResume.
|
||||
(function() {
|
||||
module.exports = function () {
|
||||
|
||||
var path = require( 'path' )
|
||||
, extend = require( './utils/extend' )
|
||||
, unused = require('./utils/string')
|
||||
, FS = require('fs')
|
||||
, _ = require('underscore')
|
||||
, FLUENT = require('./hackmyapi')
|
||||
, PATH = require('path')
|
||||
, MKDIRP = require('mkdirp')
|
||||
//, COLORS = require('colors')
|
||||
, rez, _log, _err;
|
||||
var unused = require('./utils/string')
|
||||
, PATH = require('path');
|
||||
|
||||
/**
|
||||
Given a source JSON resume, a destination resume path, and a theme file,
|
||||
generate 0..N resumes in the desired formats.
|
||||
@param src Path to the source JSON resume file: "rez/resume.json".
|
||||
@param dst An array of paths to the target resume file(s).
|
||||
@param theme Friendly name of the resume theme. Defaults to "modern".
|
||||
@param logger Optional logging override.
|
||||
*/
|
||||
function generate( src, dst, opts, logger, errHandler ) {
|
||||
|
||||
_log = logger || console.log;
|
||||
_err = errHandler || error;
|
||||
|
||||
//_opts = extend( true, _opts, opts );
|
||||
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
|
||||
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
|
||||
|
||||
// Load input resumes...
|
||||
if(!src || !src.length) { throw { fluenterror: 3 }; }
|
||||
var sheets = loadSourceResumes( src );
|
||||
|
||||
// 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;
|
||||
return extend( true, b, a );
|
||||
});
|
||||
msg && _log(msg);
|
||||
|
||||
// Verify the specified theme name/path
|
||||
var relativeThemeFolder = '../node_modules/fluent-themes/themes';
|
||||
var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme);
|
||||
var exists = require('./utils/file-exists');
|
||||
if (!exists( tFolder )) {
|
||||
tFolder = PATH.resolve( _opts.theme );
|
||||
if (!exists( tFolder )) {
|
||||
throw { fluenterror: 1, data: _opts.theme };
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Expand output resumes... (can't use map() here)
|
||||
var targets = [], that = this;
|
||||
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) {
|
||||
|
||||
var to = path.resolve(t),
|
||||
pa = path.parse(to),
|
||||
fmat = pa.ext || '.all';
|
||||
|
||||
targets.push.apply(targets, fmat === '.all' ?
|
||||
Object.keys( theTheme.formats ).map(function(k){
|
||||
var z = theTheme.formats[k];
|
||||
return { file: to.replace(/all$/g,z.outFormat), fmt: z };
|
||||
}) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
|
||||
|
||||
});
|
||||
|
||||
// Run the transformation!
|
||||
var finished = targets.map( function(t) { return single(t, theTheme); });
|
||||
|
||||
// Don't send the client back empty-handed
|
||||
return { sheet: rez, targets: targets, processed: finished };
|
||||
}
|
||||
|
||||
/**
|
||||
Generate a single resume of a specific format.
|
||||
@param f Full path to the destination resume to generate, for example,
|
||||
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
|
||||
*/
|
||||
function single( targInfo, theme ) {
|
||||
try {
|
||||
var f = targInfo.file
|
||||
, fType = targInfo.fmt.outFormat
|
||||
, fName = path.basename(f, '.' + fType)
|
||||
, theFormat;
|
||||
|
||||
// If targInfo.fmt.files exists, this theme has an explicit "files"
|
||||
// section in its theme.json file.
|
||||
if( targInfo.fmt.files && targInfo.fmt.files.length ) {
|
||||
|
||||
_log( 'Generating '.useful +
|
||||
targInfo.fmt.outFormat.toUpperCase().useful.bold +
|
||||
' resume: '.useful + path.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
|
||||
|
||||
theFormat = _fmts.filter(
|
||||
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
|
||||
MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
|
||||
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 ).replace(/\\/g,'/').useful.bold);
|
||||
|
||||
theFormat = _fmts.filter(
|
||||
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
|
||||
MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
|
||||
theFormat.gen.generate( rez, f, _opts );
|
||||
}
|
||||
}
|
||||
catch( ex ) {
|
||||
_err( ex );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handle an exception.
|
||||
*/
|
||||
function error( ex ) {
|
||||
throw ex;
|
||||
}
|
||||
|
||||
/**
|
||||
Validate 1 to N resumes in either FRESH or JSON Resume format.
|
||||
*/
|
||||
function validate( src, unused, opts, logger ) {
|
||||
_log = logger || console.log;
|
||||
if( !src || !src.length ) { throw { fluenterror: 6 }; }
|
||||
var isValid = true;
|
||||
|
||||
var validator = require('is-my-json-valid');
|
||||
var schemas = {
|
||||
fresh: require('FRESCA'),
|
||||
jars: require('./core/resume.json')
|
||||
};
|
||||
|
||||
// Load input resumes...
|
||||
var sheets = loadSourceResumes(src, function( res ) {
|
||||
try {
|
||||
return {
|
||||
file: res,
|
||||
raw: FS.readFileSync( res, 'utf8' )
|
||||
};
|
||||
}
|
||||
catch( ex ) {
|
||||
throw ex;
|
||||
}
|
||||
});
|
||||
|
||||
sheets.forEach( function( rep ) {
|
||||
|
||||
var rez;
|
||||
try {
|
||||
rez = JSON.parse( rep.raw );
|
||||
}
|
||||
catch( ex ) {
|
||||
_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( (' INTERNAL: ' + ex).red );
|
||||
}
|
||||
else {
|
||||
|
||||
_log(('ERROR: ' + ex.toString()).red.bold);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var isValid = false;
|
||||
var style = 'useful';
|
||||
var errors = [];
|
||||
var fmt = rez.meta &&
|
||||
(rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars';
|
||||
|
||||
try {
|
||||
|
||||
var validate = validator( schemas[ fmt ], { // Note [1]
|
||||
formats: {
|
||||
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
|
||||
}
|
||||
});
|
||||
|
||||
isValid = validate( rez );
|
||||
if( !isValid ) {
|
||||
style = 'warn';
|
||||
errors = validate.errors;
|
||||
}
|
||||
|
||||
}
|
||||
catch(ex) {
|
||||
|
||||
}
|
||||
|
||||
_log( 'Validating '.info + rep.file.infoBold + ' against '.info +
|
||||
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 );
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
Convert between FRESH and JRS formats.
|
||||
*/
|
||||
function convert( src, dst, opts, logger ) {
|
||||
_log = logger || console.log;
|
||||
if( !src || !src.length ) { throw { fluenterror: 6 }; }
|
||||
if( !dst || !dst.length ) {
|
||||
if( src.length === 1 ) { throw { fluenterror: 5 }; }
|
||||
else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; }
|
||||
else { throw { fluenterror: 5 }; }
|
||||
}
|
||||
if( src && dst && src.length && dst.length && src.length !== dst.length ) {
|
||||
throw { fluenterror: 7 };
|
||||
}
|
||||
var sheets = loadSourceResumes( src );
|
||||
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 +
|
||||
(' (' + 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.
|
||||
@ -289,55 +19,22 @@ Internal resume generation logic for HackMyResume.
|
||||
.useful.bold );
|
||||
}
|
||||
|
||||
function loadSourceResumes( src, fn ) {
|
||||
return src.map( function( res ) {
|
||||
_log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info +
|
||||
res.cyan.bold );
|
||||
return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res );
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
Supported resume formats.
|
||||
*/
|
||||
var _fmts = [
|
||||
{ name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() },
|
||||
{ name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() },
|
||||
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() },
|
||||
{ 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: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() }
|
||||
];
|
||||
|
||||
/**
|
||||
Default HackMyResume options.
|
||||
*/
|
||||
var _opts = {
|
||||
theme: 'modern',
|
||||
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
|
||||
indent_size: 2,
|
||||
unformatted: ['em','strong'],
|
||||
max_char: 80, // ← See lib/html.js in above-linked repo
|
||||
//wrap_line_length: 120, ← Don't use this
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
Internal module interface. Used by FCV Desktop and HMR.
|
||||
*/
|
||||
return {
|
||||
verbs: {
|
||||
build: generate,
|
||||
validate: validate,
|
||||
convert: convert,
|
||||
new: create,
|
||||
generate: require('./verbs/generate'),
|
||||
build: require('./verbs/generate'),
|
||||
validate: require('./verbs/validate'),
|
||||
convert: require('./verbs/convert'),
|
||||
create: require('./verbs/create'),
|
||||
new: require('./verbs/create'),
|
||||
help: help
|
||||
},
|
||||
lib: require('./hackmyapi'),
|
||||
options: _opts,
|
||||
formats: _fmts
|
||||
options: require('./core/default-options'),
|
||||
formats: require('./core/default-formats')
|
||||
};
|
||||
|
||||
}();
|
||||
|
13
src/index.js
13
src/index.js
@ -47,7 +47,6 @@ function main() {
|
||||
opts = getOpts( a );
|
||||
logMsg( title );
|
||||
|
||||
|
||||
// Get the action to be performed
|
||||
var params = a._.map( function(p){ return p.toLowerCase().trim(); });
|
||||
var verb = params[0];
|
||||
@ -56,7 +55,7 @@ function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get source and dest params
|
||||
// Find the TO keyword, if any
|
||||
var splitAt = _.indexOf( params, 'to' );
|
||||
if( splitAt === a._.length - 1 ) {
|
||||
// 'TO' cannot be the last argument
|
||||
@ -66,8 +65,10 @@ function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Massage inputs and outputs
|
||||
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
|
||||
var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 );
|
||||
( splitAt === -1 ) && src.length > 1 && dst.push( src.pop() ); // Allow omitting TO keyword
|
||||
var parms = [ src, dst, opts, logMsg ];
|
||||
|
||||
// Invoke the action
|
||||
@ -90,6 +91,7 @@ function getOpts( args ) {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: refactor
|
||||
function handleError( ex ) {
|
||||
var msg = '', exitCode;
|
||||
|
||||
@ -107,9 +109,10 @@ function handleError( ex ) {
|
||||
}).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 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created.'.guide; break;
|
||||
case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break;
|
||||
case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break;
|
||||
case 8: msg = 'Please '.guide + 'specify the filename of the resume'.guide.bold + ' to create.'.guide; break;
|
||||
}
|
||||
exitCode = ex.fluenterror;
|
||||
|
||||
@ -121,8 +124,10 @@ function handleError( ex ) {
|
||||
|
||||
var idx = msg.indexOf('Error: ');
|
||||
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
|
||||
if( !ex.fluenterror || ex.fluenterror < 3 )
|
||||
if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s
|
||||
console.log( ('ERROR: ' + trimmed.toString()).red.bold );
|
||||
console.log( ex.stack.gray);
|
||||
}
|
||||
else
|
||||
console.log( trimmed.toString() );
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
/**
|
||||
Definition of the `fileExists` method.
|
||||
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
|
||||
@module file-exists.js
|
||||
*/
|
||||
|
||||
var FS = require('fs');
|
||||
|
||||
// Yup, this is now the recommended way to check for file existence on Node.
|
||||
// fs.exists is deprecated and the recommended fs.statSync/lstatSync throws
|
||||
// exceptions on non-existent paths :)
|
||||
module.exports = function (path) {
|
||||
try {
|
||||
FS.statSync( path );
|
||||
return true;
|
||||
} catch( err ) {
|
||||
return !(err && err.code === 'ENOENT');
|
||||
}
|
||||
};
|
@ -11,49 +11,49 @@ Definition of the Markdown to WordProcessingML conversion routine.
|
||||
|
||||
module.exports = function( html ) {
|
||||
|
||||
var final = '';
|
||||
var is_bold = false, is_italic = false, is_link = false;
|
||||
var depth = 0;
|
||||
|
||||
// Tokenize the HTML stream.
|
||||
var tokens = HTML5Tokenizer.tokenize( html );
|
||||
|
||||
var final = '', is_bold, is_italic, is_link, link_url;
|
||||
|
||||
// Process <em>, <strong>, and <a> elements in the HTML stream, producing
|
||||
// equivalent WordProcessingML that can be dumped into a <w:p> or other
|
||||
// text container element.
|
||||
_.each( tokens, function( tok ) {
|
||||
|
||||
switch( tok.type ) {
|
||||
|
||||
case 'StartTag':
|
||||
switch( tok.tagName ) {
|
||||
case 'p':
|
||||
final += '<w:p>';
|
||||
break;
|
||||
case 'strong':
|
||||
is_bold = true;
|
||||
break;
|
||||
case 'em':
|
||||
is_italic = true;
|
||||
break;
|
||||
case 'p': final += '<w:p>'; break;
|
||||
case 'strong': is_bold = true; break;
|
||||
case 'em': is_italic = true; break;
|
||||
case 'a':
|
||||
is_link = true;
|
||||
link_url = tok.attributes.filter(function(attr){
|
||||
return attr[0] === 'href'; }
|
||||
)[0][1];
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'EndTag':
|
||||
switch( tok.tagName ) {
|
||||
case 'p':
|
||||
final += '</w:p>';
|
||||
break;
|
||||
case 'strong':
|
||||
is_bold = false;
|
||||
break;
|
||||
case 'em':
|
||||
is_italic = false;
|
||||
break;
|
||||
case 'a':
|
||||
is_link = false;
|
||||
break;
|
||||
case 'p': final += '</w:p>'; break;
|
||||
case 'strong': is_bold = false; break;
|
||||
case 'em': is_italic = false; break;
|
||||
case 'a': is_link = false; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Chars':
|
||||
var style = is_bold ? '<w:b/>' : '';
|
||||
style += is_italic ? '<w:i/>': '';
|
||||
final += '<w:r><w:rPr>' + style + '</w:rPr><w:t>' + tok.chars + '</w:t></w:r>';
|
||||
style += is_link ? '<w:rStyle w:val="Hyperlink"/>' : '';
|
||||
final +=
|
||||
(is_link ? ('<w:hlink w:dest="' + link_url + '">') : '') +
|
||||
'<w:r><w:rPr>' + style + '</w:rPr><w:t>' + tok.chars +
|
||||
'</w:t></w:r>' + (is_link ? '</w:hlink>' : '');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -17,3 +17,7 @@ String.isNullOrWhitespace = function( input ) {
|
||||
String.prototype.endsWith = function(suffix) {
|
||||
return this.indexOf(suffix, this.length - suffix.length) !== -1;
|
||||
};
|
||||
|
||||
String.is = function( val ) {
|
||||
return typeof val === 'string' || val instanceof String;
|
||||
};
|
||||
|
30
src/verbs/convert.js
Normal file
30
src/verbs/convert.js
Normal file
@ -0,0 +1,30 @@
|
||||
(function(){
|
||||
|
||||
var loadSourceResumes = require('../core/load-source-resumes');
|
||||
|
||||
/**
|
||||
Convert between FRESH and JRS formats.
|
||||
*/
|
||||
module.exports = function convert( src, dst, opts, logger ) {
|
||||
var _log = logger || console.log;
|
||||
if( !src || !src.length ) { throw { fluenterror: 6 }; }
|
||||
if( !dst || !dst.length ) {
|
||||
if( src.length === 1 ) { throw { fluenterror: 5 }; }
|
||||
else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; }
|
||||
else { throw { fluenterror: 5 }; }
|
||||
}
|
||||
if( src && dst && src.length && dst.length && src.length !== dst.length ) {
|
||||
throw { fluenterror: 7 };
|
||||
}
|
||||
var sheets = loadSourceResumes( src, _log );
|
||||
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 +
|
||||
(' (' + targetFormat + ').').useful );
|
||||
sheet.saveAs( dst[idx], targetFormat );
|
||||
});
|
||||
};
|
||||
|
||||
}());
|
22
src/verbs/create.js
Normal file
22
src/verbs/create.js
Normal file
@ -0,0 +1,22 @@
|
||||
(function(){
|
||||
|
||||
var FLUENT = require('../hackmyapi')
|
||||
, MKDIRP = require('mkdirp')
|
||||
, PATH = require('path');
|
||||
|
||||
/**
|
||||
Create a new empty resume in either FRESH or JRS format.
|
||||
*/
|
||||
module.exports = function create( src, dst, opts, logger ) {
|
||||
var _log = logger || console.log;
|
||||
if( !src || !src.length ) throw { fluenterror: 8 };
|
||||
src.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 );
|
||||
});
|
||||
};
|
||||
|
||||
}());
|
150
src/verbs/generate.js
Normal file
150
src/verbs/generate.js
Normal file
@ -0,0 +1,150 @@
|
||||
(function() {
|
||||
|
||||
var PATH = require('path')
|
||||
, parsePath = require('parse-filepath')
|
||||
, MKDIRP = require('mkdirp')
|
||||
, _opts = require('../core/default-options')
|
||||
, FluentTheme = require('../core/theme')
|
||||
, loadSourceResumes = require('../core/load-source-resumes')
|
||||
, _ = require('underscore')
|
||||
, _fmts = require('../core/default-formats')
|
||||
, _err, _log, rez;
|
||||
|
||||
/**
|
||||
Handle an exception.
|
||||
*/
|
||||
function error( ex ) {
|
||||
throw ex;
|
||||
}
|
||||
|
||||
module.exports =
|
||||
|
||||
/**
|
||||
Given a source JSON resume, a destination resume path, and a theme file,
|
||||
generate 0..N resumes in the desired formats.
|
||||
@param src Path to the source JSON resume file: "rez/resume.json".
|
||||
@param dst An array of paths to the target resume file(s).
|
||||
@param theme Friendly name of the resume theme. Defaults to "modern".
|
||||
@param logger Optional logging override.
|
||||
*/
|
||||
function generate( src, dst, opts, logger, errHandler ) {
|
||||
|
||||
_log = logger || console.log;
|
||||
_err = errHandler || error;
|
||||
|
||||
//_opts = extend( true, _opts, opts );
|
||||
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
|
||||
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
|
||||
|
||||
// Load input resumes...
|
||||
if( !src || !src.length ) { throw { fluenterror: 3 }; }
|
||||
var sheets = loadSourceResumes( src, _log );
|
||||
|
||||
// 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;
|
||||
return extend( true, b, a );
|
||||
});
|
||||
msg && _log(msg);
|
||||
|
||||
// Verify the specified theme name/path
|
||||
var relativeThemeFolder = '../../node_modules/fluent-themes/themes';
|
||||
var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme);
|
||||
var exists = require('path-exists').sync;
|
||||
if (!exists( tFolder )) {
|
||||
tFolder = PATH.resolve( _opts.theme );
|
||||
if (!exists( tFolder )) {
|
||||
throw { fluenterror: 1, data: _opts.theme };
|
||||
}
|
||||
}
|
||||
|
||||
// Load the theme
|
||||
var theTheme = (new FluentTheme()).open( tFolder );
|
||||
_opts.themeObj = theTheme;
|
||||
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
|
||||
(' theme (' + Object.keys(theTheme.formats).length + ' formats)').info);
|
||||
|
||||
// Expand output resumes... (can't use map() here)
|
||||
var targets = [], that = this;
|
||||
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) {
|
||||
|
||||
var to = PATH.resolve(t),
|
||||
pa = parsePath(to),
|
||||
fmat = pa.extname || '.all';
|
||||
|
||||
targets.push.apply(targets, fmat === '.all' ?
|
||||
Object.keys( theTheme.formats ).map(function(k){
|
||||
var z = theTheme.formats[k];
|
||||
return { file: to.replace(/all$/g,z.outFormat), fmt: z };
|
||||
}) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
|
||||
|
||||
});
|
||||
|
||||
// Run the transformation!
|
||||
var finished = targets.map( function(t) { return single(t, theTheme); });
|
||||
|
||||
// Don't send the client back empty-handed
|
||||
return { sheet: rez, targets: targets, processed: finished };
|
||||
};
|
||||
|
||||
/**
|
||||
Generate a single resume of a specific format.
|
||||
@param f Full path to the destination resume to generate, for example,
|
||||
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
|
||||
*/
|
||||
function single( targInfo, theme ) {
|
||||
try {
|
||||
var f = targInfo.file
|
||||
, fType = targInfo.fmt.outFormat
|
||||
, fName = PATH.basename(f, '.' + fType)
|
||||
, theFormat;
|
||||
|
||||
// If targInfo.fmt.files exists, this theme has an explicit "files"
|
||||
// section in its theme.json file.
|
||||
if( targInfo.fmt.files && targInfo.fmt.files.length ) {
|
||||
|
||||
_log( 'Generating '.useful +
|
||||
targInfo.fmt.outFormat.toUpperCase().useful.bold +
|
||||
' resume: '.useful + PATH.relative(process.cwd(), f ).replace(/\\/g,'/').useful.bold);
|
||||
|
||||
theFormat = _fmts.filter(
|
||||
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
|
||||
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
|
||||
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 ).replace(/\\/g,'/').useful.bold);
|
||||
|
||||
theFormat = _fmts.filter(
|
||||
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
|
||||
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
|
||||
theFormat.gen.generate( rez, f, _opts );
|
||||
}
|
||||
}
|
||||
catch( ex ) {
|
||||
_err( ex );
|
||||
}
|
||||
}
|
||||
|
||||
}());
|
97
src/verbs/validate.js
Normal file
97
src/verbs/validate.js
Normal file
@ -0,0 +1,97 @@
|
||||
(function() {
|
||||
|
||||
var FS = require('fs');
|
||||
var loadSourceResumes = require('../core/load-source-resumes');
|
||||
|
||||
module.exports =
|
||||
|
||||
/**
|
||||
Validate 1 to N resumes in either FRESH or JSON Resume format.
|
||||
*/
|
||||
function validate( src, unused, opts, logger ) {
|
||||
var _log = logger || console.log;
|
||||
if( !src || !src.length ) { throw { fluenterror: 6 }; }
|
||||
var isValid = true;
|
||||
|
||||
var validator = require('is-my-json-valid');
|
||||
var schemas = {
|
||||
fresh: require('fresca'),
|
||||
jars: require('../core/resume.json')
|
||||
};
|
||||
|
||||
// Load input resumes...
|
||||
var sheets = loadSourceResumes(src, _log, function( res ) {
|
||||
try {
|
||||
return {
|
||||
file: res,
|
||||
raw: FS.readFileSync( res, 'utf8' )
|
||||
};
|
||||
}
|
||||
catch( ex ) {
|
||||
throw ex;
|
||||
}
|
||||
});
|
||||
|
||||
sheets.forEach( function( rep ) {
|
||||
|
||||
var rez;
|
||||
try {
|
||||
rez = JSON.parse( rep.raw );
|
||||
}
|
||||
catch( ex ) { // Note [1]
|
||||
_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( (' INTERNAL: ' + ex).red );
|
||||
}
|
||||
else {
|
||||
|
||||
_log(('ERROR: ' + ex.toString()).red.bold);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var isValid = false;
|
||||
var style = 'useful';
|
||||
var errors = [];
|
||||
var fmt = rez.meta &&
|
||||
(rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars';
|
||||
|
||||
try {
|
||||
|
||||
var validate = validator( schemas[ fmt ], { // Note [1]
|
||||
formats: {
|
||||
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
|
||||
}
|
||||
});
|
||||
|
||||
isValid = validate( rez );
|
||||
if( !isValid ) {
|
||||
style = 'warn';
|
||||
errors = validate.errors;
|
||||
}
|
||||
|
||||
}
|
||||
catch(ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
_log( 'Validating '.info + rep.file.infoBold + ' against '.info +
|
||||
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 );
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
}());
|
91
tests/resumes/jrs-0.0.0/empty.json
Normal file
91
tests/resumes/jrs-0.0.0/empty.json
Normal file
@ -0,0 +1,91 @@
|
||||
{
|
||||
"basics": {
|
||||
"name": "",
|
||||
"label": "",
|
||||
"picture": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"degree": "",
|
||||
"website": "",
|
||||
"summary": "",
|
||||
"location": {
|
||||
"address": "",
|
||||
"postalCode": "",
|
||||
"city": "",
|
||||
"countryCode": "",
|
||||
"region": ""
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"network": "",
|
||||
"username": "",
|
||||
"url": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"work": [
|
||||
{
|
||||
"company": "",
|
||||
"position": "",
|
||||
"website": "",
|
||||
"startDate": "",
|
||||
"endDate": "",
|
||||
"summary": "",
|
||||
"highlights": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"awards": [
|
||||
{
|
||||
"title": "",
|
||||
"date": "",
|
||||
"awarder": "",
|
||||
"summary": ""
|
||||
}
|
||||
],
|
||||
"education": [
|
||||
{
|
||||
"institution": "",
|
||||
"area": "",
|
||||
"studyType": "",
|
||||
"startDate": "",
|
||||
"endDate": "",
|
||||
"gpa": "",
|
||||
"courses": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"publications": [
|
||||
{
|
||||
"name": "",
|
||||
"publisher": "",
|
||||
"releaseDate": "",
|
||||
"website": "",
|
||||
"summary": ""
|
||||
}
|
||||
],
|
||||
"volunteer": [
|
||||
{
|
||||
"organization": "",
|
||||
"position": "",
|
||||
"website": "",
|
||||
"startDate": "",
|
||||
"endDate": "",
|
||||
"summary": "",
|
||||
"highlights": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"name": "",
|
||||
"level": "",
|
||||
"keywords": [
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
104
tests/resumes/jrs-0.0.0/jane-incomplete.json
Normal file
104
tests/resumes/jrs-0.0.0/jane-incomplete.json
Normal file
@ -0,0 +1,104 @@
|
||||
{
|
||||
"basics": {
|
||||
"name": "Jane Q. Fullstacker"
|
||||
},
|
||||
"education": [
|
||||
{
|
||||
"gpa": "3.5",
|
||||
"courses": [
|
||||
"Course 1",
|
||||
"Course 2",
|
||||
"Course 2"
|
||||
],
|
||||
"startDate": "2005-09",
|
||||
"endDate": "2008-05"
|
||||
},
|
||||
{
|
||||
"institution": "Medfield College",
|
||||
"gpa": "3.2",
|
||||
"courses": [
|
||||
"Course 1",
|
||||
"Course 2",
|
||||
"Course 2"
|
||||
],
|
||||
"startDate": "2003-09",
|
||||
"endDate": "2005-06"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"name": "Web Dev",
|
||||
"keywords": [
|
||||
"JavaScript",
|
||||
"HTML 5",
|
||||
"CSS",
|
||||
"LAMP",
|
||||
"MVC",
|
||||
"REST"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "JavaScript"
|
||||
}
|
||||
],
|
||||
"volunteer": [],
|
||||
"publications": [
|
||||
{
|
||||
"name": "Building User Interfaces with Electron and Atom",
|
||||
"releaseDate": "2011",
|
||||
"website": "http://codeproject.com/build-ui-electron-atom.aspx"
|
||||
},
|
||||
{
|
||||
"name": "Jane Fullstacker's Blog",
|
||||
"publisher": "self",
|
||||
"releaseDate": "2011",
|
||||
"website": "http://janef.me"
|
||||
},
|
||||
{
|
||||
"name": "Teach Yourself GORFF in 21 Days",
|
||||
"publisher": "Amazon"
|
||||
}
|
||||
],
|
||||
"interests": [
|
||||
{
|
||||
"name": "reading",
|
||||
"summary": "Jane is a fan of mystery novels and courtroom dramas including Agatha Christie and John Grisham.",
|
||||
"keywords": [
|
||||
"mystery",
|
||||
"Agatha Christie",
|
||||
"John Grisham"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hiking",
|
||||
"summary": "Jane enjoys hiking, light mountain climbing, and has four summits under her belt!"
|
||||
},
|
||||
{
|
||||
"name": "yoga"
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"name": "John Davidson",
|
||||
"reference": "Jane is awesome! I'd hire her again in a heartbeat."
|
||||
},
|
||||
{
|
||||
"name": "Elias Fullstacker",
|
||||
"reference": "I worked with Jane on Jabberwocky and can vouch for her awesome technical capabilities and attention to detail. Insta-hire."
|
||||
},
|
||||
{
|
||||
"name": "Dana Nevins",
|
||||
"reference": "I've known Jane personally and professionally for almost ten years. She is one in a million."
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"language": "English",
|
||||
"level": "Native"
|
||||
},
|
||||
{
|
||||
"language": "Spanish",
|
||||
"level": "Moderate"
|
||||
}
|
||||
]
|
||||
}
|
76
tests/test-cli.js
Normal file
76
tests/test-cli.js
Normal file
@ -0,0 +1,76 @@
|
||||
|
||||
var chai = require('chai')
|
||||
, expect = chai.expect
|
||||
, should = chai.should()
|
||||
, path = require('path')
|
||||
, _ = require('underscore')
|
||||
, FRESHResume = require('../src/core/fresh-resume')
|
||||
, FCMD = require( '../src/hackmycmd')
|
||||
, validator = require('is-my-json-valid')
|
||||
, COLORS = require('colors');
|
||||
|
||||
chai.config.includeStack = false;
|
||||
|
||||
describe('Testing CLI interface', function () {
|
||||
|
||||
var _sheet;
|
||||
|
||||
function logMsg() {
|
||||
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
var opts = {
|
||||
//theme: 'compact',
|
||||
format: 'FRESH',
|
||||
prettify: true,
|
||||
silent: true
|
||||
};
|
||||
|
||||
var opts2 = {
|
||||
format: 'JRS',
|
||||
prettify: true,
|
||||
silent: true
|
||||
};
|
||||
|
||||
run( 'new', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
|
||||
run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
|
||||
run( 'new', ['tests/sandbox/new-1.json', 'tests/sandbox/new-2.json', 'tests/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' );
|
||||
run( 'new', ['tests/sandbox/new-jrs-1.json', 'tests/sandbox/new-jrs-2.json', 'tests/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' );
|
||||
run( 'new', ['tests/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
|
||||
fail( 'new', [], [], opts, " (when a filename isn't specified)" );
|
||||
|
||||
run( 'validate', ['node_modules/jane-q-fullstacker/resume/jane-resume.json'], [], opts, ' (FRESH format)' );
|
||||
run( 'validate', ['tests/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
|
||||
|
||||
function run( verb, src, dst, opts, msg ) {
|
||||
msg = msg || '.';
|
||||
it( 'The ' + verb.toUpperCase() + ' command should SUCCEED' + msg, function () {
|
||||
function runIt() {
|
||||
FCMD.verbs[verb]( src, dst, opts, opts.silent ? logMsg : null );
|
||||
}
|
||||
runIt.should.not.Throw();
|
||||
});
|
||||
}
|
||||
|
||||
function fail( verb, src, dst, opts, msg ) {
|
||||
msg = msg || '.';
|
||||
it( 'The ' + verb.toUpperCase() + ' command should FAIL' + msg, function () {
|
||||
function runIt() {
|
||||
FCMD.verbs[verb]( src, dst, opts, logMsg );
|
||||
}
|
||||
runIt.should.Throw();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
@ -18,7 +18,7 @@ describe('FRESH/JRS converter', function () {
|
||||
|
||||
it('should round-trip from JRS to FRESH to JRS without modifying or losing data', function () {
|
||||
|
||||
var fileA = path.join( __dirname, 'resumes/jrs/richard-hendriks.json' );
|
||||
var fileA = path.join( __dirname, 'resumes/jrs-0.0.0/richard-hendriks.json' );
|
||||
var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' );
|
||||
|
||||
_sheet = new FRESHResume().open( fileA );
|
||||
|
@ -55,7 +55,7 @@ describe('jane-doe.json (FRESH)', function () {
|
||||
|
||||
it('should validate against the FRESH resume schema', function() {
|
||||
var result = _sheet.isValid();
|
||||
// var schemaJson = require('FRESCA');
|
||||
// var schemaJson = require('fresca');
|
||||
// var validate = validator( schemaJson, { verbose: true } );
|
||||
// var result = validate( JSON.parse( _sheet.imp.raw ) );
|
||||
result || console.log("\n\nOops, resume didn't validate. " +
|
||||
|
@ -9,55 +9,61 @@ var chai = require('chai')
|
||||
|
||||
chai.config.includeStack = false;
|
||||
|
||||
describe('jane-doe.json (JRS)', function () {
|
||||
function testResume( opts ) {
|
||||
|
||||
describe( opts.title + ' (JRS)', function() {
|
||||
|
||||
opts.isValid = opts.isValid !== false;
|
||||
|
||||
var _sheet;
|
||||
|
||||
it('should open without throwing an exception', function () {
|
||||
it('should open without throwing an exception', function () {
|
||||
var that = this;
|
||||
function tryOpen() {
|
||||
_sheet = new JRSResume().open(
|
||||
path.join( __dirname, 'resumes/jrs/jane-q-fullstacker.json' ) );
|
||||
path.join( __dirname, 'resumes/jrs-0.0.0/' + opts.title + '.json' ) );
|
||||
}
|
||||
tryOpen.should.not.Throw();
|
||||
});
|
||||
|
||||
it('should have one or more of each section', function() {
|
||||
expect(
|
||||
(_sheet.basics) &&
|
||||
(_sheet.work && _sheet.work.length > 0) &&
|
||||
(_sheet.skills && _sheet.skills.length > 0) &&
|
||||
(_sheet.education && _sheet.education.length > 0) &&
|
||||
(_sheet.volunteer && _sheet.volunteer.length > 0) &&
|
||||
(_sheet.publications && _sheet.publications.length > 0) &&
|
||||
(_sheet.awards && _sheet.awards.length > 0)
|
||||
).to.equal( true );
|
||||
var newObj = _.pick( _sheet, opts.sections );
|
||||
expect( Object.keys(newObj).length ).to.equal( opts.sections.length );
|
||||
});
|
||||
|
||||
it('should have a work duration of 7 years', function() {
|
||||
_sheet.basics.computed.numYears.should.equal( 7 );
|
||||
it('should have a work duration of ' + opts.duration + ' years', function() {
|
||||
_sheet.basics.computed.numYears.should.equal( opts.duration );
|
||||
});
|
||||
|
||||
it('should save without throwing an exception', function(){
|
||||
it('should save without throwing an exception', function() {
|
||||
var that = this;
|
||||
function trySave() {
|
||||
_sheet.save( 'tests/sandbox/jane-q-fullstacker.json' );
|
||||
_sheet.save( 'tests/sandbox/' + opts.title + '.json' );
|
||||
}
|
||||
trySave.should.not.Throw();
|
||||
});
|
||||
|
||||
it('should not be modified after saving', function() {
|
||||
var savedSheet = new JRSResume().open( 'tests/sandbox/jane-q-fullstacker.json' );
|
||||
var savedSheet = new JRSResume().open( 'tests/sandbox/' + opts.title + '.json' );
|
||||
_sheet.stringify().should.equal( savedSheet.stringify() )
|
||||
});
|
||||
|
||||
it('should validate against the JSON Resume schema', function() {
|
||||
it('should ' + (opts.isValid ? '' : 'NOT ') + 'validate against the JSON Resume schema', function() {
|
||||
var result = _sheet.isValid();
|
||||
// var schemaJson = require('../src/core/resume.json');
|
||||
// var validate = validator( schemaJson, { verbose: true } );
|
||||
// var result = validate( JSON.parse( _sheet.imp.raw ) );
|
||||
result || console.log("\n\nOops, resume didn't validate. " +
|
||||
"Validation errors:\n\n", _sheet.basics.imp.validationErrors, "\n\n");
|
||||
result.should.equal( true );
|
||||
result.should.equal( opts.isValid );
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
var sects = [ 'basics', 'work', 'volunteer', 'skills', 'education', 'publications', 'awards', 'references' ];
|
||||
|
||||
testResume({ title: 'jane-q-fullstacker', duration: 7, sections: sects });
|
||||
testResume({ title: 'jane-incomplete', duration: 0, sections: _.without(sects, 'awards', 'work') });
|
||||
testResume({ title: 'richard-hendriks', duration: 1, sections: sects });
|
||||
testResume({ title: 'empty', duration: 0, sections: [], isValid: false });
|
||||
|
@ -35,9 +35,9 @@ describe('Testing themes', function () {
|
||||
theme: themeName,
|
||||
format: 'FRESH',
|
||||
prettify: true,
|
||||
silent: false
|
||||
silent: true
|
||||
};
|
||||
FCMD.verbs.build( src, dst, opts );
|
||||
FCMD.verbs.build( src, dst, opts, function() { } );
|
||||
}
|
||||
tryOpen.should.not.Throw();
|
||||
});
|
||||
|
Reference in New Issue
Block a user