1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2025-05-11 08:17:07 +01:00

Compare commits

...

7 Commits

196 changed files with 6303 additions and 12927 deletions

View File

@ -1,4 +1,3 @@
src/
assets/
test/
doc/

View File

@ -33,22 +33,14 @@ will reference your local installation (you may need to
## Making changes
1. HackMyResume sources live in the [`/src`][src] folder. Always make your edits
there, never in the generated `/dist` folder.
1. HackMyResume sources live in the [`/src`][src] folder.
2. After making your changes, run `grunt build` to package the HackMyResume
sources to the `/dist` folder. This will transform CoffeeScript files to
JavaScript and perform other build steps as necessary. In the future, a watch
task or guardfile will be added to automate this step.
3. Do local spot testing with `hackmyresume` as normal.
4. When you're ready to submit your changes, run `grunt test` to run the HMR
2. When you're ready to submit your changes, run `grunt test` to run the HMR
test suite. Fix any errors that occur.
5. Commit and push your changes.
3. Commit and push your changes.
6. Submit a pull request targeting the HackMyResume `dev` branch.
4. Submit a pull request targeting the HackMyResume `dev` branch.
[node]: https://nodejs.org/en/

View File

@ -6,28 +6,6 @@ module.exports = function (grunt) {
pkg: grunt.file.readJSON('package.json'),
copy: {
main: {
expand: true,
cwd: 'src',
src: ['**/*','!**/*.coffee'],
dest: 'dist/',
}
},
coffee: {
main: {
options: {
sourceMap: true
},
expand: true,
cwd: 'src',
src: ['**/*.coffee'],
dest: 'dist/',
ext: '.js'
}
},
simplemocha: {
options: {
globals: ['expect', 'should'],
@ -40,19 +18,16 @@ module.exports = function (grunt) {
},
clean: {
test: ['test/sandbox'],
dist: ['dist']
test: ['test/sandbox']
},
eslint: {
target: ['Gruntfile.js', 'dist/cli/**/*.js', 'test/*.js']
target: ['Gruntfile.js', 'src/**/*.js', 'test/*.js']
}
};
grunt.initConfig( opts );
grunt.loadNpmTasks('grunt-contrib-coffee');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-simple-mocha');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-contrib-clean');
@ -67,11 +42,11 @@ module.exports = function (grunt) {
// Use 'grunt build' to build HMR
grunt.registerTask('build', 'Build the HackMyResume application.',
function() {
grunt.task.run( ['clean:dist','copy','coffee','eslint'] );
grunt.task.run( ['eslint'] );
}
);
// Default task does everything
grunt.registerTask('default', [ 'test', 'document' ]);
grunt.registerTask('default', [ 'test' ]);
};

30
dist/cli/analyze.hbs vendored
View File

@ -1,30 +0,0 @@
{{style "SECTIONS (" "bold"}}{{style totals.numSections "white" }}{{style ")" "bold"}}
employment: {{v totals.totals.employment "-" 2 "bold" }}
projects: {{v totals.totals.projects "-" 2 "bold" }}
education: {{v totals.totals.education "-" 2 "bold" }}
service: {{v totals.totals.service "-" 2 "bold" }}
skills: {{v totals.totals.skills "-" 2 "bold" }}
writing: {{v totals.totals.writing "-" 2 "bold" }}
speaking: {{v totals.totals.speaking "-" 2 "bold" }}
reading: {{v totals.totals.reading "-" 2 "bold" }}
social: {{v totals.totals.social "-" 2 "bold" }}
references: {{v totals.totals.references "-" 2 "bold" }}
testimonials: {{v totals.totals.testimonials "-" 2 "bold" }}
languages: {{v totals.totals.languages "-" 2 "bold" }}
interests: {{v totals.totals.interests "-" 2 "bold" }}
{{style "COVERAGE (" "bold"}}{{style coverage.pct "white"}}{{style ")" "bold"}}
Total Days: {{v coverage.duration.total "-" 5 "bold" }}
Employed: {{v coverage.duration.work "-" 5 "bold" }}
Gaps: {{v coverage.gaps.length "-" 5 "bold" }} [{{#if coverage.gaps.length }}{{#each coverage.gaps }}{{#unless @first}} {{/unless}}{{gapLength duration }}{{/each}}{{/if}}]
Overlaps: {{v coverage.overlaps.length "-" 5 "bold" }} [{{#if coverage.overlaps.length }}{{#each coverage.overlaps }}{{#unless @first}} {{/unless}}{{gapLength duration }}{{/each}}{{/if}}]
{{style "KEYWORDS (" "bold"}}{{style keywords.length "white" }}{{style ")" "bold"}}
{{#each keywords }}{{{pad name 18}}}: {{v count "-" 5 "bold"}} mention{{#isPlural count}}s{{/isPlural}}
{{/each}}
-------------------------------
{{v keywords.length "0" 9 "bold"}} {{style "KEYWORDS" "bold"}} {{v keywords.totalKeywords "0" 5 "bold"}} {{style "mentions" "bold"}}

287
dist/cli/error.js vendored
View File

@ -1,287 +0,0 @@
(function() {
/**
Error-handling routines for HackMyResume.
@module cli/error
@license MIT. See LICENSE.md for details.
*/
var FS, HMSTATUS, M2C, PATH, SyntaxErrorEx, WRAP, _defaultLog, assembleError, chalk, extend, printf;
HMSTATUS = require('../core/status-codes');
FS = require('fs');
PATH = require('path');
WRAP = require('word-wrap');
M2C = require('../utils/md2chalk');
chalk = require('chalk');
extend = require('extend');
printf = require('printf');
SyntaxErrorEx = require('../utils/syntax-error-ex');
require('string.prototype.startswith');
/** Error handler for HackMyResume. All errors are handled here.
@class ErrorHandler */
module.exports = {
init: function(debug, assert, silent) {
this.debug = debug;
this.assert = assert;
this.silent = silent;
this.msgs = require('./msg').errors;
return this;
},
err: function(ex, shouldExit) {
var o, objError, stack, stackTrace;
// Short-circuit logging output if --silent is on
o = this.silent ? function() {} : _defaultLog;
if (ex.pass) {
// Special case; can probably be removed.
throw ex;
}
// Load error messages
this.msgs = this.msgs || require('./msg').errors;
// Handle packaged HMR exceptions
if (ex.fluenterror) {
// Output the error message
objError = assembleError.call(this, ex);
o(this['format_' + objError.etype](objError.msg));
// Output the stack (sometimes)
if (objError.withStack) {
stack = ex.stack || (ex.inner && ex.inner.stack);
stack && o(chalk.gray(stack));
}
if (shouldExit || ex.exit) {
if (this.debug) {
o(chalk.cyan('Exiting with error code ' + ex.fluenterror.toString()));
}
if (this.assert) {
ex.pass = true;
throw ex;
}
return process.exit(ex.fluenterror);
}
} else {
// Handle raw exceptions
o(ex);
stackTrace = ex.stack || (ex.inner && ex.inner.stack);
if (stackTrace && this.debug) {
return o(M2C(ex.stack || ex.inner.stack, 'gray'));
}
}
},
format_error: function(msg) {
msg = msg || '';
return chalk.red.bold(msg.toUpperCase().startsWith('ERROR:') ? msg : 'Error: ' + msg);
},
format_warning: function(brief, msg) {
return chalk.yellow(brief) + chalk.yellow(msg || '');
},
format_custom: function(msg) {
return msg;
}
};
_defaultLog = function() {
return console.log.apply(console.log, arguments); // eslint-disable-line no-console
};
assembleError = function(ex) {
var etype, msg, quit, se, withStack;
msg = '';
withStack = false;
quit = false;
etype = 'warning';
if (this.debug) {
withStack = true;
}
switch (ex.fluenterror) {
case HMSTATUS.themeNotFound:
msg = printf(M2C(this.msgs.themeNotFound.msg, 'yellow'), ex.data);
break;
case HMSTATUS.copyCSS:
msg = M2C(this.msgs.copyCSS.msg, 'red');
quit = false;
break;
case HMSTATUS.resumeNotFound:
//msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' );
msg += M2C(FS.readFileSync(PATH.resolve(__dirname, 'help/' + ex.verb + '.txt'), 'utf8'), 'white', 'yellow');
break;
case HMSTATUS.missingCommand:
// msg = M2C( this.msgs.missingCommand.msg + " (", 'yellow');
// msg += Object.keys( FCMD.verbs ).map( (v, idx, ar) ->
// return ( if idx == ar.length - 1 then chalk.yellow('or ') else '') +
// chalk.yellow.bold(v.toUpperCase());
// ).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += M2C(FS.readFileSync(PATH.resolve(__dirname, 'help/use.txt'), 'utf8'), 'white', 'yellow');
break;
case HMSTATUS.invalidCommand:
msg = printf(M2C(this.msgs.invalidCommand.msg, 'yellow'), ex.attempted);
break;
case HMSTATUS.resumeNotFoundAlt:
msg = M2C(this.msgs.resumeNotFoundAlt.msg, 'yellow');
break;
case HMSTATUS.inputOutputParity:
msg = M2C(this.msgs.inputOutputParity.msg);
break;
case HMSTATUS.createNameMissing:
msg = M2C(this.msgs.createNameMissing.msg);
break;
case HMSTATUS.pdfGeneration:
msg = M2C(this.msgs.pdfGeneration.msg, 'bold');
if (ex.inner) {
msg += chalk.red('\n' + ex.inner);
}
quit = false;
etype = 'error';
break;
case HMSTATUS.invalid:
msg = M2C(this.msgs.invalid.msg, 'red');
etype = 'error';
break;
case HMSTATUS.generateError:
msg = (ex.inner && ex.inner.toString()) || ex;
quit = false;
etype = 'error';
break;
case HMSTATUS.fileSaveError:
msg = printf(M2C(this.msgs.fileSaveError.msg), (ex.inner || ex).toString());
etype = 'error';
quit = false;
break;
case HMSTATUS.invalidFormat:
ex.data.forEach(function(d) {
return msg += printf(M2C(this.msgs.invalidFormat.msg, 'bold'), ex.theme.name.toUpperCase(), d.format.toUpperCase());
}, this);
break;
case HMSTATUS.missingParam:
msg = printf(M2C(this.msgs.missingParam.msg), ex.expected, ex.helper);
break;
case HMSTATUS.invalidHelperUse:
msg = printf(M2C(this.msgs.invalidHelperUse.msg), ex.helper);
if (ex.error) {
msg += '\n--> ' + assembleError.call(this, extend(true, {}, ex, {
fluenterror: ex.error
})).msg;
}
quit = false;
etype = 'warning';
break;
case HMSTATUS.notOnPath:
msg = printf(M2C(this.msgs.notOnPath.msg, 'bold'), ex.engine);
quit = false;
etype = 'error';
break;
case HMSTATUS.readError:
if (!ex.quiet) {
// eslint-disable-next-line no-console
console.error(printf(M2C(this.msgs.readError.msg, 'red'), ex.file));
}
msg = ex.inner.toString();
etype = 'error';
break;
case HMSTATUS.mixedMerge:
msg = M2C(this.msgs.mixedMerge.msg);
quit = false;
break;
case HMSTATUS.invokeTemplate:
msg = M2C(this.msgs.invokeTemplate.msg, 'red');
msg += M2C('\n' + WRAP(ex.inner.toString(), {
width: 60,
indent: ' '
}), 'gray');
etype = 'custom';
break;
case HMSTATUS.compileTemplate:
etype = 'error';
break;
case HMSTATUS.themeLoad:
msg = M2C(printf(this.msgs.themeLoad.msg, ex.attempted.toUpperCase()), 'red');
if (ex.inner && ex.inner.fluenterror) {
msg += M2C('\nError: ', 'red') + assembleError.call(this, ex.inner).msg;
}
quit = true;
etype = 'custom';
break;
case HMSTATUS.parseError:
if (SyntaxErrorEx.is(ex.inner)) {
// eslint-disable-next-line no-console
console.error(printf(M2C(this.msgs.readError.msg, 'red'), ex.file));
se = new SyntaxErrorEx(ex, ex.raw);
if ((se.line != null) && (se.col != null)) {
msg = printf(M2C(this.msgs.parseError.msg[0], 'red'), se.line, se.col);
} else if (se.line != null) {
msg = printf(M2C(this.msgs.parseError.msg[1], 'red'), se.line);
} else {
msg = M2C(this.msgs.parseError.msg[2], 'red');
}
} else if (ex.inner && (ex.inner.line != null) && (ex.inner.col != null)) {
msg = printf(M2C(this.msgs.parseError.msg[0], 'red'), ex.inner.line, ex.inner.col);
} else {
msg = ex;
}
etype = 'error';
break;
case HMSTATUS.createError:
// inner.code could be EPERM, EACCES, etc
msg = printf(M2C(this.msgs.createError.msg), ex.inner.path);
etype = 'error';
break;
case HMSTATUS.validateError:
msg = printf(M2C(this.msgs.validateError.msg), ex.inner.toString());
etype = 'error';
break;
case HMSTATUS.invalidOptionsFile:
msg = M2C(this.msgs.invalidOptionsFile.msg[0]);
if (SyntaxErrorEx.is(ex.inner)) {
// eslint-disable-next-line no-console
console.error(printf(M2C(this.msgs.readError.msg, 'red'), ex.file));
se = new SyntaxErrorEx(ex, ex.raw);
if ((se.line != null) && (se.col != null)) {
msg += printf(M2C(this.msgs.parseError.msg[0], 'red'), se.line, se.col);
} else if (se.line != null) {
msg += printf(M2C(this.msgs.parseError.msg[1], 'red'), se.line);
} else {
msg += M2C(this.msgs.parseError.msg[2], 'red');
}
} else if (ex.inner && (ex.inner.line != null) && (ex.inner.col != null)) {
msg += printf(M2C(this.msgs.parseError.msg[0], 'red'), ex.inner.line, ex.inner.col);
} else {
msg += ex;
}
msg += this.msgs.invalidOptionsFile.msg[1];
etype = 'error';
break;
case HMSTATUS.optionsFileNotFound:
msg = M2C(this.msgs.optionsFileNotFound.msg);
etype = 'error';
break;
case HMSTATUS.unknownSchema:
msg = M2C(this.msgs.unknownSchema.msg[0]);
//msg += "\n" + M2C( @msgs.unknownSchema.msg[1], 'yellow' )
etype = 'error';
break;
case HMSTATUS.themeHelperLoad:
msg = printf(M2C(this.msgs.themeHelperLoad.msg), ex.glob);
etype = 'error';
break;
case HMSTATUS.invalidSchemaVersion:
msg = printf(M2C(this.msgs.invalidSchemaVersion.msg), ex.data);
etype = 'error';
}
return {
msg: msg, // The error message to display
withStack: withStack, // Whether to include the stack
quit: quit,
etype: etype
};
};
}).call(this);
//# sourceMappingURL=error.js.map

View File

@ -1,25 +0,0 @@
**analyze** | Analyze a resume for statistical insight
Usage:
**hackmyresume ANALYZE <resume>**
The ANALYZE command evaluates the specified resume(s) for
coverage, duration, gaps, keywords, and other metrics.
This command can be run against multiple resumes. Each
will be analyzed in turn.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified, separated by spaces.
hackmyresume ANALYZE resume.json
hackmyresume ANALYZE r1.json r2.json r3.json
Options:
**None.**

View File

@ -1,69 +0,0 @@
**build** | Generate themed resumes in multiple formats
Usage:
**hackmyresume BUILD <resume> TO <target> [--theme]**
**[--pdf] [--no-escape] [--private]**
The BUILD command generates themed resumes and CVs in
multiple formats. Use it to create outbound resumes in
specific formats such HTML, MS Word, and PDF.
Parameters:
**<resume>**
Path to a FRESH or JRS resume (*.json) containing your
resume data. Multiple resumes may be specified.
If multiple resumes are specified, they will be merged
into a single resume prior to transformation.
**<target>**
Path to the desired output resume. Multiple resumes
may be specified. The file extension will determine
the format.
.all Generate all supported formats
.html HTML 5
.doc MS Word
.pdf Adobe Acrobat PDF
.txt plain text
.md Markdown
.png PNG Image
.latex LaTeX
Note: not all formats are supported by all themes!
Check the theme's documentation for details or use
the .all extension to build all available formats.
Options:
**--theme -t <theme-name-or-path>**
Path to a FRESH or JSON Resume theme OR the name of a
built-in theme. Valid theme names are 'modern',
'positive', 'compact', 'awesome', and 'basis'.
**--pdf -p <engine>**
Specify the PDF engine to use. Legal values are
'none', 'wkhtmltopdf', 'phantom', or 'weasyprint'.
**--no-escape**
Disable escaping / encoding of resume data during
resume generation. Handlebars themes only.
**--private**
Include resume fields marked as private.
Notes:
The BUILD command can be run against multiple source as well
as multiple target resumes. If multiple source resumes are
provided, they will be merged into a single source resume
before generation. If multiple output resumes are provided,
each will be generated in turn.

View File

@ -1,33 +0,0 @@
**convert** | Convert resumes between FRESH and JRS formats
Usage:
**hackmyresume CONVERT <resume> TO <target> [--format]**
The CONVERT command converts one or more resume documents
between the FRESH Resume Schema and JSON Resume formats.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified.
**<targets>**
The path of the converted resume. Multiple resumes can
be specified, one per provided input resume.
Options:
**--format -f <fmt>**
The desired format for the new resume(s). Valid values
are 'FRESH', 'JRS', or, to target the latest edge
version of the JSON Resume Schema, 'JRS@1'.
If this parameter is omitted, the destination format
will be inferred from the source resume's format. If
the source format is FRESH, the destination format
will be JSON Resume, and vice-versa.

View File

@ -1,23 +0,0 @@
**help** | View help on a specific HackMyResume command
Usage:
**hackmyresume HELP [<command>]**
The HELP command displays help information for a specific
HackMyResume command, including the HELP command itself.
Parameters:
**<command>**
The HackMyResume command to view help information for.
Must be BUILD, NEW, CONVERT, ANALYZE, VALIDATE, PEEK,
or HELP.
hackmyresume help convert
hackmyresume help help
Options:
**None.**

29
dist/cli/help/new.txt vendored
View File

@ -1,29 +0,0 @@
**new** | Create a new FRESH or JRS resume document
Usage:
**hackmyresume NEW <fileName> [--format]**
The NEW command generates a new resume document in FRESH
or JSON Resume format. This document can serve as an
official source of truth for your resume and career data
as well an input to tools like HackMyResume.
Parameters:
**<fileName>**
The filename (relative or absolute path) of the resume
to be created. Multiple resume paths can be specified,
and each will be created in turn.
hackmyresume NEW resume.json
hackmyresume NEW r1.json foo/r2.json ../r3.json
Options:
**--format -f <fmt>**
The desired format for the new resume(s). Valid values
are 'FRESH', 'JRS', or, to target the latest edge
version of the JSON Resume Schema, 'JRS@1'.

View File

@ -1,31 +0,0 @@
**peek** | View portions of a resume from the command line
Usage:
**hackmyresume PEEK <resume> <at>**
The PEEK command displays a specific piece or part of the
resume without requiring the resume to be opened in an
editor.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified, separated by spaces.
hackmyresume PEEK r1.json r2.json r3.json "employment.history[2]"
**<at>**
The resume property or field to be displayed. Can be
any valid resume path, for example:
education[0]
info.name
employment.history[3].start
Options:
**None.**

70
dist/cli/help/use.txt vendored
View File

@ -1,70 +0,0 @@
**HackMyResume** | A Swiss Army knife for resumes and CVs
Usage:
**hackmyresume [--version] [--help] [--silent] [--debug]**
**[--options] [--no-colors] <command> [<args>]**
Commands: (type "hackmyresume help COMMAND" for details)
**BUILD** Build your resume to the destination format(s).
**ANALYZE** Analyze your resume for keywords, gaps, and metrics.
**VALIDATE** Validate your resume for errors and typos.
**NEW** Create a new resume in FRESH or JSON Resume format.
**CONVERT** Convert your resume between FRESH and JSON Resume.
**PEEK** View a specific field or element on your resume.
**HELP** View help on a specific HackMyResume command.
Common Tasks:
Generate a resume in a specific format (HTML, Word, PDF, etc.)
**hackmyresume build rez.json to out/rez.html**
**hackmyresume build rez.json to out/rez.doc**
**hackmyresume build rez.json to out/rez.pdf**
**hackmyresume build rez.json to out/rez.txt**
**hackmyresume build rez.json to out/rez.md**
**hackmyresume build rez.json to out/rez.png**
**hackmyresume build rez.json to out/rez.tex**
Build a resume to ALL available formats:
**hackmyresume build rez.json to out/rez.all**
Build a resume with a specific theme:
**hackmyresume build rez.json to out/rez.all -t themeName**
Create a new empty resume:
**hackmyresume new rez.json**
Convert a resume between FRESH and JRS formats:
**hackmyresume convert rez.json converted.json**
Analyze a resume for important metrics
**hackmyresume analyze rez.json**
Find more resume themes:
**https://www.npmjs.com/search?q=jsonresume-theme**
**https://www.npmjs.com/search?q=fresh-theme**
**https://github.com/fresh-standard/fresh-themes**
Validate a resume's structure and syntax:
**hackmyresume validate resume.json**
View help on a specific command:
**hackmyresume help [build|convert|new|analyze|validate|peek|help]**
Submit a bug or request:
**https://githut.com/hacksalot/HackMyResume/issues**
HackMyResume is free and open source software published
under the MIT license. For more information, visit the
HackMyResume website or GitHub project page.

View File

@ -1,26 +0,0 @@
**validate** | Validate a resume for correctness
Usage:
**hackmyresume VALIDATE <resume> [--assert]**
The VALIDATE command validates a FRESH or JRS document
against its governing schema, verifying that the resume
is correctly structured and formatted.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified.
hackmyresume ANALYZE resume.json
hackmyresume ANALYZE r1.json r2.json r3.json
Options:
**--assert -a**
Tell HackMyResume to return a non-zero process exit
code if a resume fails to validate.

22
dist/cli/index.js vendored
View File

@ -1,22 +0,0 @@
#! /usr/bin/env node
/**
Command-line interface (CLI) for HackMyResume.
@license MIT. See LICENSE.md for details.
@module index.js
*/
try {
require('./main')( process.argv );
}
catch( ex ) {
require('./error').err( ex, true );
}

371
dist/cli/main.js vendored
View File

@ -1,371 +0,0 @@
(function() {
/**
Definition of the `main` function.
@module cli/main
@license MIT. See LICENSE.md for details.
*/
/* Invoke a HackMyResume verb. */
/* Success handler for verb invocations. Calls process.exit by default */
/* Init options prior to setting up command infrastructure. */
/* Massage command-line args and setup Commander.js. */
/*
Initialize HackMyResume options.
TODO: Options loading is a little hacky, for two reasons:
- Commander.js idiosyncracies
- Need to accept JSON inputs from the command line.
*/
/* Simple logging placeholder. */
/* Split multiple command-line filenames by the 'TO' keyword */
var Command, EXTEND, FS, HMR, HMSTATUS, M2C, OUTPUT, PAD, PATH, PKG, _, _err, _exitCallback, _opts, _out, _title, chalk, execute, executeFail, executeSuccess, initOptions, initialize, loadOptions, logMsg, printf, safeLoadJSON, splitSrcDest;
HMR = require('../index');
PKG = require('../../package.json');
FS = require('fs');
EXTEND = require('extend');
chalk = require('chalk');
PATH = require('path');
HMSTATUS = require('../core/status-codes');
safeLoadJSON = require('../utils/safe-json-loader');
//StringUtils = require '../utils/string.js'
_ = require('underscore');
OUTPUT = require('./out');
PAD = require('string-padding');
Command = require('commander').Command;
M2C = require('../utils/md2chalk');
printf = require('printf');
_opts = {};
_title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***');
_out = new OUTPUT(_opts);
_err = require('./error');
_exitCallback = null;
/*
A callable implementation of the HackMyResume CLI. Encapsulates the command
line interface as a single method accepting a parameter array.
@alias module:cli/main.main
@param rawArgs {Array} An array of command-line parameters. Will either be
process.argv (in production) or custom parameters (in test).
*/
module.exports = function(rawArgs, exitCallback) {
var args, initInfo, program;
initInfo = initialize(rawArgs, exitCallback);
if (initInfo === null) {
return;
}
args = initInfo.args;
// Create the top-level (application) command...
program = new Command('hackmyresume').version(PKG.version).description(chalk.yellow.bold('*** HackMyResume ***')).option('-s --silent', 'Run in silent mode').option('--no-color', 'Disable colors').option('--color', 'Enable colors').option('-d --debug', 'Enable diagnostics', false).option('-a --assert', 'Treat warnings as errors', false).option('-v --version', 'Show the version').allowUnknownOption();
program.jsonArgs = initInfo.options;
// Create the NEW command
program.command('new').arguments('<sources...>').option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH').alias('create').description('Create resume(s) in FRESH or JSON RESUME format.').action((function(sources) {
execute.call(this, sources, [], this.opts(), logMsg);
}));
// Create the VALIDATE command
program.command('validate').arguments('<sources...>').description('Validate a resume in FRESH or JSON RESUME format.').action(function(sources) {
execute.call(this, sources, [], this.opts(), logMsg);
});
// Create the CONVERT command
program.command('convert').description('Convert a resume to/from FRESH or JSON RESUME format.').option('-f --format <fmt>', 'FRESH or JRS format and optional version', void 0).action(function() {
var x;
x = splitSrcDest.call(this);
execute.call(this, x.src, x.dst, this.opts(), logMsg);
});
// Create the ANALYZE command
program.command('analyze').arguments('<sources...>').option('--private', 'Include resume fields marked as private', false).description('Analyze one or more resumes.').action(function(sources) {
execute.call(this, sources, [], this.opts(), logMsg);
});
// Create the PEEK command
//.action(( sources, sectionOrField ) ->
program.command('peek').arguments('<sources...>').description('Peek at a resume field or section').action(function(sources) {
var dst;
dst = (sources && sources.length > 1) ? [sources.pop()] : [];
execute.call(this, sources, dst, this.opts(), logMsg);
});
// Create the BUILD command
//.action(( sources, targets, options ) ->
program.command('build').alias('generate').option('-t --theme <theme>', 'Theme name or path').option('-n --no-prettify', 'Disable HTML prettification', true).option('-c --css <option>', 'CSS linking / embedding').option('-p --pdf <engine>', 'PDF generation engine').option('--no-sort', 'Sort resume sections by date', false).option('--tips', 'Display theme tips and warnings.', false).option('--private', 'Include resume fields marked as private', false).option('--no-escape', 'Turn off encoding in Handlebars themes.', false).description('Generate resume to multiple formats').action(function() {
var x;
x = splitSrcDest.call(this);
execute.call(this, x.src, x.dst, this.opts(), logMsg);
});
// Create the HELP command
program.command('help').arguments('[command]').description('Get help on a HackMyResume command').action(function(cmd) {
var manPage;
cmd = cmd || 'use';
manPage = FS.readFileSync(PATH.join(__dirname, 'help/' + cmd + '.txt'), 'utf8');
_out.log(M2C(manPage, 'white', 'yellow.bold'));
});
program.parse(args);
if (!program.args.length) {
throw {
fluenterror: 4
};
}
};
initialize = function(ar, exitCallback) {
var o;
_exitCallback = exitCallback || process.exit;
o = initOptions(ar);
if (o.ex) {
_err.init(false, true, false);
if (o.ex.op === 'parse') {
_err.err({
fluenterror: o.ex.op === 'parse' ? HMSTATUS.invalidOptionsFile : HMSTATUS.optionsFileNotFound,
inner: o.ex.inner,
quit: true
});
} else {
_err.err({
fluenterror: HMSTATUS.optionsFileNotFound,
inner: o.ex.inner,
quit: true
});
}
return null;
}
o.silent || logMsg(_title);
// Emit debug prelude if --debug was specified
if (o.debug) {
_out.log(chalk.cyan('The -d or --debug switch was specified. DEBUG mode engaged.'));
_out.log('');
_out.log(chalk.cyan(PAD(' Platform:', 25, null, PAD.RIGHT)) + chalk.cyan.bold(process.platform === 'win32' ? 'windows' : process.platform));
_out.log(chalk.cyan(PAD(' Node.js:', 25, null, PAD.RIGHT)) + chalk.cyan.bold(process.version));
_out.log(chalk.cyan(PAD(' HackMyResume:', 25, null, PAD.RIGHT)) + chalk.cyan.bold('v' + PKG.version));
_out.log(chalk.cyan(PAD(' FRESCA:', 25, null, PAD.RIGHT)) + chalk.cyan.bold(PKG.dependencies.fresca));
//_out.log(chalk.cyan(PAD(' fresh-themes:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-themes'] ))
//_out.log(chalk.cyan(PAD(' fresh-jrs-converter:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-jrs-converter'] ))
_out.log('');
}
_err.init(o.debug, o.assert, o.silent);
// Handle invalid verbs here (a bit easier here than in commander.js)...
if (o.verb && !HMR.verbs[o.verb] && !HMR.alias[o.verb] && o.verb !== 'help') {
_err.err({
fluenterror: HMSTATUS.invalidCommand,
quit: true,
attempted: o.orgVerb
}, true);
}
// Override the .missingArgument behavior
Command.prototype.missingArgument = function(/* unused */) {
if (this.name() !== 'help') {
_err.err({
verb: this.name(),
fluenterror: HMSTATUS.resumeNotFound
}, true);
}
};
// Override the .helpInformation behavior
Command.prototype.helpInformation = function() {
var manPage;
manPage = FS.readFileSync(PATH.join(__dirname, 'help/use.txt'), 'utf8');
return M2C(manPage, 'white', 'yellow');
};
return {
args: o.args,
options: o.json
};
};
initOptions = function(ar) {
oVerb;
/* jshint ignore:end */
var args, cleanArgs, inf, isAssert, isDebug, isMono, isNoEscape, isSilent, oJSON, oVerb, optStr, optsIdx, verb, vidx;
verb = '';
args = ar.slice();
cleanArgs = args.slice(2);
oJSON;
if (cleanArgs.length) {
// Support case-insensitive sub-commands (build, generate, validate, etc)
vidx = _.findIndex(cleanArgs, function(v) {
return v[0] !== '-';
});
if (vidx !== -1) {
oVerb = cleanArgs[vidx];
verb = args[vidx + 2] = oVerb.trim().toLowerCase();
}
// Remove --options --opts -o and process separately
optsIdx = _.findIndex(cleanArgs, function(v) {
return v === '-o' || v === '--options' || v === '--opts';
});
if (optsIdx !== -1) {
optStr = cleanArgs[optsIdx + 1];
args.splice(optsIdx + 2, 2);
if (optStr && (optStr = optStr.trim())) {
//var myJSON = JSON.parse(optStr);
if (optStr[0] === '{') {
// TODO: remove use of evil(). - hacksalot
/* jshint ignore:start */
oJSON = eval('(' + optStr + ')'); // jshint ignore:line <-- no worky
} else {
inf = safeLoadJSON(optStr);
if (!inf.ex) {
oJSON = inf.json;
} else {
return inf;
}
}
}
}
}
// Grab the --debug flag, --silent, --assert and --no-color flags
isDebug = _.some(args, function(v) {
return v === '-d' || v === '--debug';
});
isSilent = _.some(args, function(v) {
return v === '-s' || v === '--silent';
});
isAssert = _.some(args, function(v) {
return v === '-a' || v === '--assert';
});
isMono = _.some(args, function(v) {
return v === '--no-color';
});
isNoEscape = _.some(args, function(v) {
return v === '--no-escape';
});
return {
color: !isMono,
debug: isDebug,
silent: isSilent,
assert: isAssert,
noescape: isNoEscape,
orgVerb: oVerb,
verb: verb,
json: oJSON,
args: args
};
};
execute = function(src, dst, opts, log) {
var prom, v;
// Create the verb
v = new HMR.verbs[this.name()]();
// Initialize command-specific options
loadOptions.call(this, opts, this.parent.jsonArgs);
// Set up error/output handling
_opts.errHandler = v;
_out.init(_opts);
// Hook up event notifications
v.on('hmr:status', function() {
return _out.do.apply(_out, arguments);
});
v.on('hmr:error', function() {
return _err.err.apply(_err, arguments);
});
// Invoke the verb using promise syntax
prom = v.invoke.call(v, src, dst, _opts, log);
prom.then(executeSuccess, executeFail);
};
executeSuccess = function(/*obj*/) {};
// Can't call _exitCallback here (process.exit) when PDF is running in BK
//_exitCallback 0; return
/* Failure handler for verb invocations. Calls process.exit by default */
executeFail = function(err) {
var finalErrorCode, msgs;
//console.dir err
finalErrorCode = -1;
if (err) {
if (err.fluenterror) {
finalErrorCode = err.fluenterror;
} else if (err.length) {
finalErrorCode = err[0].fluenterror;
} else {
finalErrorCode = err;
}
}
if (_opts.debug) {
msgs = require('./msg').errors;
logMsg(printf(M2C(msgs.exiting.msg, 'cyan'), finalErrorCode));
if (err.stack) {
logMsg(err.stack);
}
}
_exitCallback(finalErrorCode);
};
loadOptions = function(o, cmdO) {
// o and this.opts() seem to be the same (command-specific options)
// Load the specified options file (if any) and apply options
if (cmdO) {
o = EXTEND(true, o, cmdO);
}
// Merge in command-line options
o = EXTEND(true, o, this.opts());
// Kludge parent-level options until piping issue is resolved
if (this.parent.silent !== void 0 && this.parent.silent !== null) {
o.silent = this.parent.silent;
}
if (this.parent.debug !== void 0 && this.parent.debug !== null) {
o.debug = this.parent.debug;
}
if (this.parent.assert !== void 0 && this.parent.assert !== null) {
o.assert = this.parent.assert;
}
if (o.debug) {
logMsg(chalk.cyan('OPTIONS:') + '\n');
_.each(o, function(val, key) {
return logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'), PAD(key, 22, null, PAD.RIGHT), val);
});
logMsg('');
}
EXTEND(true, _opts, o);
};
splitSrcDest = function() {
var params, splitAt;
params = this.parent.args.filter(function(j) {
return String.is(j);
});
if (params.length === 0) {
throw {
//tmpName = @name()
fluenterror: HMSTATUS.resumeNotFound,
verb: this.name(),
quit: true
};
}
// Find the TO keyword, if any
splitAt = _.findIndex(params, function(p) {
return p.toLowerCase() === 'to';
});
// TO can't be the last keyword
if (splitAt === params.length - 1 && splitAt !== -1) {
logMsg(chalk.yellow('Please ') + chalk.yellow.bold('specify an output file') + chalk.yellow(' for this operation or ') + chalk.yellow.bold('omit the TO keyword') + chalk.yellow('.'));
return;
}
return {
src: params.slice(0, splitAt === -1 ? void 0 : splitAt),
dst: splitAt === -1 ? [] : params.slice(splitAt + 1)
};
};
logMsg = function() {
// eslint-disable-next-line no-console
return _opts.silent || console.log.apply(console.log, arguments);
};
}).call(this);
//# sourceMappingURL=main.js.map

17
dist/cli/msg.js vendored
View File

@ -1,17 +0,0 @@
(function() {
/**
Message-handling routines for HackMyResume.
@module cli/msg
@license MIT. See LICENSE.md for details.
*/
var PATH, YAML;
PATH = require('path');
YAML = require('yamljs');
module.exports = YAML.load(PATH.join(__dirname, 'msg.yml'));
}).call(this);
//# sourceMappingURL=msg.js.map

141
dist/cli/msg.yml vendored
View File

@ -1,141 +0,0 @@
events:
begin:
msg: Invoking **%s** command.
beforeCreate:
msg: Creating new **%s** resume: **%s**
afterCreate:
msg: Creating new **%s** resume: **%s**
afterRead:
msg: Reading **%s** resume: **%s**
beforeTheme:
msg: Verifying **%s** theme.
afterTheme:
msg: Verifying outputs: ???
beforeMerge:
msg:
- "Merging **%s**"
- " onto **%s**"
applyTheme:
msg: Applying **%s** theme (**%s** format%s)
afterBuild:
msg:
- "The **%s** theme says:"
- |
"For best results view JSON Resume themes over a
local or remote HTTP connection. For example:
npm install http-server -g
http-server <resume-folder>
For more information, see the README."
afterGenerate:
msg:
- " (with %s)"
- "Skipping %s resume: %s"
- "Generating **%s** resume: **%s**"
beforeAnalyze:
msg: "Analyzing **%s** resume: **%s**"
beforeConvert:
msg: "Converting **%s** (**%s**) to **%s** (**%s**)"
afterValidate:
msg:
- "Validating **%s** against the **%s** schema: "
- "VALID!"
- "INVALID"
- "BROKEN"
- "MISSING"
- "ERROR"
beforePeek:
msg:
- Peeking at **%s** in **%s**
- Peeking at **%s**
afterPeek:
msg: "The specified key **%s** was not found in **%s**."
afterInlineConvert:
msg: Converting **%s** to **%s** format.
errors:
themeNotFound:
msg: >
**Couldn't find the '%s' theme.** Please specify the name of a preinstalled
FRESH theme or the path to a locally installed FRESH or JSON Resume theme.
copyCSS:
msg: Couldn't copy CSS file to destination folder.
resumeNotFound:
msg: Please **feed me a resume** in FRESH or JSON Resume format.
missingCommand:
msg: Please **give me a command**
invalidCommand:
msg: Invalid command: '%s'
resumeNotFoundAlt:
msg: Please **feed me a resume** in either FRESH or JSON Resume format.
inputOutputParity:
msg: Please **specify an output file name** for every input file you wish to convert.
createNameMissing:
msg: Please **specify the filename** of the resume to create.
pdfGeneration:
msg: PDF generation failed. Make sure wkhtmltopdf is installed and accessible from your path.
invalid:
msg: Validation failed and the --assert option was specified.
invalidFormat:
msg: The **%s** theme doesn't support the **%s** format.
notOnPath:
msg: %s wasn't found on your system path or is inaccessible. PDF not generated.
readError:
msg: Reading **???** resume: **%s**
parseError:
msg:
- Invalid or corrupt JSON on line %s column %s.
- Invalid or corrupt JSON on line %s.
- Invalid or corrupt JSON.
invalidHelperUse:
msg: "**Warning**: Incorrect use of the **%s** theme helper."
fileSaveError:
msg: An error occurred while writing %s to disk: %s.
mixedMerge:
msg: "**Warning:** merging mixed resume types. Errors may occur."
invokeTemplate:
msg: "An error occurred during template invocation."
compileTemplate:
msg: "An error occurred during template compilation."
themeLoad:
msg: "Applying **%s** theme (? formats)"
invalidParamCount:
msg: "Invalid number of parameters. Expected: **%s**."
missingParam:
msg: The '**%s**' parameter was needed but not supplied.
createError:
msg: Failed to create **'%s'**.
exiting:
msg: Exiting with status code **%s**.
validateError:
msg: "An error occurred during validation:\n%s"
invalidOptionsFile:
msg:
- "The specified options file is invalid:\n"
- "\nMake sure the options file contains valid JSON."
optionsFileNotFound:
msg: "The specified options file is missing or inaccessible."
unknownSchema:
msg:
- "Unknown resume schema. Did you specify a valid FRESH or JRS resume?"
- |
At a minimum, a FRESH resume must include a "name" field and a "meta"
property.
"name": "John Doe",
"meta": {
"format": "FRESH@0.1.0"
}
JRS-format resumes must include a "basics" section with a "name":
"basics": {
"name": "John Doe"
}
themeHelperLoad:
msg: >-
An error occurred while attempting to load the '%s' theme helper. Is the
theme correctly installed?
dummy: dontcare
invalidSchemaVersion:
msg: "'%s' is not recognized as a valid schema version."

190
dist/cli/out.js vendored
View File

@ -1,190 +0,0 @@
(function() {
/**
Output routines for HackMyResume.
@license MIT. See LICENSE.md for details.
@module cli/out
*/
var EXTEND, FS, HANDLEBARS, HME, M2C, OutputHandler, PATH, YAML, _, chalk, dbgStyle, pad, printf;
chalk = require('chalk');
HME = require('../core/event-codes');
_ = require('underscore');
M2C = require('../utils/md2chalk.js');
PATH = require('path');
FS = require('fs');
EXTEND = require('extend');
HANDLEBARS = require('handlebars');
YAML = require('yamljs');
printf = require('printf');
pad = require('string-padding');
dbgStyle = 'cyan';
OutputHandler = class OutputHandler {
constructor(opts) {
this.init(opts);
return;
}
init(opts) {
this.opts = EXTEND(true, this.opts || {}, opts);
this.msgs = YAML.load(PATH.join(__dirname, 'msg.yml')).events;
}
log() {
var finished;
printf = require('printf');
finished = printf.apply(printf, arguments);
return this.opts.silent || console.log(finished); // eslint-disable-line no-console
}
do(evt) {
var L, adj, info, msg, msgs, numFormats, output, rawTpl, sty, style, suffix, template, that, themeName, tot;
that = this;
L = function() {
return that.log.apply(that, arguments);
};
switch (evt.sub) {
case HME.begin:
return this.opts.debug && L(M2C(this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase());
//when HME.beforeCreate
//L( M2C( this.msgs.beforeCreate.msg, 'green' ), evt.fmt, evt.file )
//break;
case HME.afterCreate:
L(M2C(this.msgs.beforeCreate.msg, evt.isError ? 'red' : 'green'), evt.fmt, evt.file);
break;
case HME.beforeTheme:
return this.opts.debug && L(M2C(this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase());
case HME.afterParse:
return L(M2C(this.msgs.afterRead.msg, 'gray', 'white.dim'), evt.fmt.toUpperCase(), evt.file);
case HME.beforeMerge:
msg = '';
evt.f.reverse().forEach(function(a, idx) {
return msg += printf((idx === 0 ? this.msgs.beforeMerge.msg[0] : this.msgs.beforeMerge.msg[1]), a.file);
}, this);
return L(M2C(msg, (evt.mixed ? 'yellow' : 'gray'), 'white.dim'));
case HME.applyTheme:
this.theme = evt.theme;
numFormats = Object.keys(evt.theme.formats).length;
return L(M2C(this.msgs.applyTheme.msg, evt.status === 'error' ? 'red' : 'gray', evt.status === 'error' ? 'bold' : 'white.dim'), evt.theme.name.toUpperCase(), numFormats, numFormats === 1 ? '' : 's');
case HME.end:
if (evt.cmd === 'build') {
themeName = this.theme.name.toUpperCase();
if (this.opts.tips && (this.theme.message || this.theme.render)) {
if (this.theme.message) {
L(M2C(this.msgs.afterBuild.msg[0], 'cyan'), themeName);
return L(M2C(this.theme.message, 'white'));
} else if (this.theme.render) {
L(M2C(this.msgs.afterBuild.msg[0], 'cyan'), themeName);
return L(M2C(this.msgs.afterBuild.msg[1], 'white'));
}
}
}
break;
case HME.afterGenerate:
suffix = '';
if (evt.fmt === 'pdf') {
if (this.opts.pdf) {
if (this.opts.pdf !== 'none') {
suffix = printf(M2C(this.msgs.afterGenerate.msg[0], evt.error ? 'red' : 'green'), this.opts.pdf);
} else {
L(M2C(this.msgs.afterGenerate.msg[1], 'gray'), evt.fmt.toUpperCase(), evt.file);
return;
}
}
}
return L(M2C(this.msgs.afterGenerate.msg[2] + suffix, evt.error ? 'red' : 'green'), pad(evt.fmt.toUpperCase(), 4, null, pad.RIGHT), PATH.relative(process.cwd(), evt.file));
case HME.beforeAnalyze:
return L(M2C(this.msgs.beforeAnalyze.msg, 'green'), evt.fmt, evt.file);
case HME.afterAnalyze:
info = evt.info;
rawTpl = FS.readFileSync(PATH.join(__dirname, 'analyze.hbs'), 'utf8');
HANDLEBARS.registerHelper(require('../helpers/console-helpers'));
template = HANDLEBARS.compile(rawTpl, {
strict: false,
assumeObjects: false
});
tot = 0;
info.keywords.forEach(function(g) {
return tot += g.count;
});
info.keywords.totalKeywords = tot;
output = template(info);
return this.log(chalk.cyan(output));
case HME.beforeConvert:
return L(M2C(this.msgs.beforeConvert.msg, evt.error ? 'red' : 'green'), evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt);
case HME.afterInlineConvert:
return L(M2C(this.msgs.afterInlineConvert.msg, 'gray', 'white.dim'), evt.file, evt.fmt);
case HME.afterValidate:
style = 'red';
adj = '';
msgs = this.msgs.afterValidate.msg;
switch (evt.status) {
case 'valid':
style = 'green';
adj = msgs[1];
break;
case 'invalid':
style = 'yellow';
adj = msgs[2];
break;
case 'broken':
style = 'red';
adj = msgs[3];
break;
case 'missing':
style = 'red';
adj = msgs[4];
break;
case 'unknown':
style = 'red';
adj = msgs[5];
}
evt.schema = evt.schema.replace('jars', 'JSON Resume').toUpperCase();
L(M2C(msgs[0], 'white') + chalk[style].bold(adj), evt.file, evt.schema);
if (evt.violations) {
_.each(evt.violations, function(err) {
L(chalk.yellow.bold('--> ') + chalk.yellow(err.field.replace('data.', 'resume.').toUpperCase() + ' ' + err.message));
}, this);
}
break;
case HME.afterPeek:
sty = evt.error ? 'red' : (evt.target !== void 0 ? 'green' : 'yellow');
// "Peeking at 'someKey' in 'someFile'."
if (evt.requested) {
L(M2C(this.msgs.beforePeek.msg[0], sty), evt.requested, evt.file);
} else {
L(M2C(this.msgs.beforePeek.msg[1], sty), evt.file);
}
// If the key was present, print it
if (evt.target !== void 0 && !evt.error) {
// eslint-disable-next-line no-console
return console.dir(evt.target, {
depth: null,
colors: true
});
} else if (!evt.error) {
return L(M2C(this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file);
} else if (evt.error) {
return L(chalk.red(evt.error.inner.inner));
}
}
}
};
module.exports = OutputHandler;
}).call(this);
//# sourceMappingURL=out.js.map

View File

@ -1,66 +0,0 @@
(function() {
/*
Event code definitions.
@module core/default-formats
@license MIT. See LICENSE.md for details.
*/
/** Supported resume formats. */
module.exports = [
{
name: 'html',
ext: 'html',
gen: new (require('../generators/html-generator'))()
},
{
name: 'txt',
ext: 'txt',
gen: new (require('../generators/text-generator'))()
},
{
name: 'doc',
ext: 'doc',
fmt: 'xml',
gen: new (require('../generators/word-generator'))()
},
{
name: 'pdf',
ext: 'pdf',
fmt: 'html',
is: false,
gen: new (require('../generators/html-pdf-cli-generator'))()
},
{
name: 'png',
ext: 'png',
fmt: 'html',
is: false,
gen: new (require('../generators/html-png-generator'))()
},
{
name: 'md',
ext: 'md',
fmt: 'txt',
gen: new (require('../generators/markdown-generator'))()
},
{
name: 'json',
ext: 'json',
gen: new (require('../generators/json-generator'))()
},
{
name: 'yml',
ext: 'yml',
fmt: 'yml',
gen: new (require('../generators/json-yaml-generator'))()
},
{
name: 'latex',
ext: 'tex',
fmt: 'latex',
gen: new (require('../generators/latex-generator'))()
}
];
}).call(this);
//# sourceMappingURL=default-formats.js.map

View File

@ -1,20 +0,0 @@
(function() {
/*
Event code definitions.
@module core/default-options
@license MIT. See LICENSE.md for details.
*/
module.exports = {
theme: 'modern',
prettify: {
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
}).call(this);
//# sourceMappingURL=default-options.js.map

View File

@ -1,77 +0,0 @@
{
"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": [""]
}]
}

View File

@ -1,42 +0,0 @@
(function() {
/*
Event code definitions.
@module core/event-codes
@license MIT. See LICENSE.md for details.
*/
module.exports = {
error: -1,
success: 0,
begin: 1,
end: 2,
beforeRead: 3,
afterRead: 4,
beforeCreate: 5,
afterCreate: 6,
beforeTheme: 7,
afterTheme: 8,
beforeMerge: 9,
afterMerge: 10,
beforeGenerate: 11,
afterGenerate: 12,
beforeAnalyze: 13,
afterAnalyze: 14,
beforeConvert: 15,
afterConvert: 16,
verifyOutputs: 17,
beforeParse: 18,
afterParse: 19,
beforePeek: 20,
afterPeek: 21,
beforeInlineConvert: 22,
afterInlineConvert: 23,
beforeValidate: 24,
afterValidate: 25,
beforeWrite: 26,
afterWrite: 27,
applyTheme: 28
};
}).call(this);
//# sourceMappingURL=event-codes.js.map

View File

@ -1,101 +0,0 @@
(function() {
/**
The HackMyResume date representation.
@license MIT. See LICENSE.md for details.
@module core/fluent-date
*/
var FluentDate, abbr, moment, months;
moment = require('moment');
require('../utils/string');
/**
Create a FluentDate from a string or Moment date object. There are a few date
formats to be aware of here.
1. The words "Present" and "Now", referring to the current date
2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10")
3. Year-and-month only ("2015-04")
4. Year-only "YYYY" ("2015")
5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008")
6. Empty dates ("", " ")
7. Any other date format that Moment.js can parse from
Note: Moment can transparently parse all or most of these, without requiring us
to specify a date format...but for maximum parsing safety and to avoid Moment
deprecation warnings, it's recommended to either a) explicitly specify the date
format or b) use an ISO format. For clarity, we handle these cases explicitly.
@class FluentDate
*/
FluentDate = class FluentDate {
constructor(dt) {
this.rep = this.fmt(dt);
}
static isCurrent(dt) {
return !dt || (String.is(dt) && /^(present|now|current)$/.test(dt));
}
};
months = {};
abbr = {};
moment.months().forEach(function(m, idx) {
return months[m.toLowerCase()] = idx + 1;
});
moment.monthsShort().forEach(function(m, idx) {
return abbr[m.toLowerCase()] = idx + 1;
});
abbr.sept = 9;
module.exports = FluentDate;
FluentDate.fmt = function(dt, throws) {
var month, mt, parts, ref, temp;
throws = (throws === void 0 || throws === null) || throws;
if (typeof dt === 'string' || dt instanceof String) {
dt = dt.toLowerCase().trim();
if (/^(present|now|current)$/.test(dt)) { // "Present", "Now"
return moment();
} else if (/^\D+\s+\d{4}$/.test(dt)) { // "Mar 2015"
parts = dt.split(' ');
month = months[parts[0]] || abbr[parts[0]];
temp = parts[1] + '-' + ((ref = month < 10) != null ? ref : '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');
} else if (/^\s*\d{4}\s*$/.test(dt)) { // "2015"
return moment(dt, 'YYYY');
} else if (/^\s*$/.test(dt)) { // "", " "
return moment();
} else {
mt = moment(dt);
if (mt.isValid()) {
return mt;
}
if (throws) {
throw 'Invalid date format encountered.';
}
return null;
}
} else {
if (!dt) {
return moment();
} else if (dt.isValid && dt.isValid()) {
return dt;
}
if (throws) {
throw 'Unknown date object encountered.';
}
return null;
}
};
}).call(this);
//# sourceMappingURL=fluent-date.js.map

View File

@ -1,478 +0,0 @@
(function() {
/**
Definition of the FRESHResume class.
@license MIT. See LICENSE.md for details.
@module core/fresh-resume
*/
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
var CONVERTER, FS, FluentDate, FreshResume, JRSResume, MD, PATH, XML, _, __, _parseDates, extend, moment, validator;
FS = require('fs');
extend = require('extend');
validator = require('is-my-json-valid');
_ = require('underscore');
__ = require('lodash');
PATH = require('path');
moment = require('moment');
XML = require('xml-escape');
MD = require('marked');
CONVERTER = require('fresh-jrs-converter');
JRSResume = require('./jrs-resume');
FluentDate = require('./fluent-date');
/**
A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
object is an instantiation of that JSON decorated with utility methods.
@constructor
*/
FreshResume = class FreshResume { // extends AbstractResume
/** Initialize the the FreshResume from JSON string data. */
parse(stringData, opts) {
var ref;
this.imp = (ref = this.imp) != null ? ref : {
raw: stringData
};
return this.parseJSON(JSON.parse(stringData), opts);
}
/**
Initialize the FreshResume from JSON.
Open and parse the specified FRESH resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
*/
parseJSON(rep, opts) {
var ignoreList, privateList, ref, scrubbed, scrubber;
if (opts && opts.privatize) {
// Ignore any element with the 'ignore: true' or 'private: true' designator.
scrubber = require('../utils/resume-scrubber');
({scrubbed, ignoreList, privateList} = scrubber.scrubResume(rep, opts));
}
// Now apply the resume representation onto this object
extend(true, this, opts && opts.privatize ? scrubbed : rep);
if (!((ref = this.imp) != null ? ref.processed : void 0)) {
// Set up metadata TODO: Clean up metadata on the object model.
opts = opts || {};
if (opts.imp === void 0 || opts.imp) {
this.imp = this.imp || {};
this.imp.title = (opts.title || this.imp.title) || this.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
}
}
this.imp.processed = true;
// Parse dates, sort dates, and calculate computed values
(opts.date === void 0 || opts.date) && _parseDates.call(this);
(opts.sort === void 0 || opts.sort) && this.sort();
(opts.compute === void 0 || opts.compute) && (this.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
}
return this;
}
/** Save the sheet to disk (for environments that have disk access). */
save(filename) {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
return this;
}
/**
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
saveAs(filename, format) {
var newRep, parts, safeFormat, useEdgeSchema;
// If format isn't specified, default to FRESH
safeFormat = (format && format.trim()) || 'FRESH';
// Validate against the FRESH version regex
// freshVersionReg = require '../utils/fresh-version-regex'
// if (not freshVersionReg().test( safeFormat ))
// throw badVer: safeFormat
parts = safeFormat.split('@');
if (parts[0] === 'FRESH') {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
} else if (parts[0] === 'JRS') {
useEdgeSchema = parts.length > 1 ? parts[1] === '1' : false;
newRep = CONVERTER.toJRS(this, {
edge: useEdgeSchema
});
FS.writeFileSync(filename, JRSResume.stringify(newRep), 'utf8');
} else {
throw {
badVer: safeFormat
};
}
return this;
}
/**
Duplicate this FreshResume instance.
This method first extend()s this object onto an empty, creating a deep copy,
and then passes the result into a new FreshResume instance via .parseJSON.
We do it this way to create a true clone of the object without re-running any
of the associated processing.
*/
dupe() {
var jso, rnew;
jso = extend(true, {}, this);
rnew = new FreshResume();
rnew.parseJSON(jso, {});
return rnew;
}
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way.
*/
stringify() {
return FreshResume.stringify(this);
}
/**
Create a copy of this resume in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder).
TODO: Move this out of FRESHResume.
*/
transformStrings(filt, transformer) {
var ret, trx;
ret = this.dupe();
trx = require('../utils/string-transformer');
return trx(ret, filt, transformer);
}
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
markdownify() {
var MDIN, trx;
MDIN = function(txt) {
return MD(txt || '').replace(/^\s*<p>|<\/p>\s*$/gi, '');
};
trx = function(key, val) {
if (key === 'summary') {
return MD(val);
}
return MDIN(val);
};
return this.transformStrings(['skills', 'url', 'start', 'end', 'date'], trx);
}
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
xmlify() {
var trx;
trx = function(key, val) {
return XML(val);
};
return this.transformStrings([], trx);
}
/** Return the resume format. */
format() {
return 'FRESH';
}
/**
Return internal metadata. Create if it doesn't exist.
*/
i() {
return this.imp = this.imp || {};
}
/**
Return a unique list of all skills declared in the resume.
*/
// TODO: Several problems here:
// 1) Confusing name. Easily confused with the keyword-inspector module, which
// parses resume body text looking for these same keywords. This should probably
// be renamed.
// 2) Doesn't bother trying to integrate skills.list with skills.sets if they
// happen to declare different skills, and if skills.sets declares ONE skill and
// skills.list declared 50, only 1 skill will be registered.
// 3) In the future, skill.sets should only be able to use skills declared in
// skills.list. That is, skills.list is the official record of a candidate's
// declared skills. skills.sets is just a way of grouping those into skillsets
// for easier consumption.
keywords() {
var flatSkills;
flatSkills = [];
if (this.skills) {
if (this.skills.sets) {
flatSkills = this.skills.sets.map(function(sk) {
return sk.skills;
}).reduce(function(a, b) {
return a.concat(b);
});
} else if (this.skills.list) {
flatSkills = flatSkills.concat(this.skills.list.map(function(sk) {
return sk.name;
}));
}
flatSkills = _.uniq(flatSkills);
}
return flatSkills;
}
/**
Reset the sheet to an empty state. TODO: refactor/review
*/
clear(clearMeta) {
clearMeta = ((clearMeta === void 0) && true) || clearMeta;
if (clearMeta) {
delete this.imp;
}
delete this.computed; // Don't use Object.keys() here
delete this.employment;
delete this.service;
delete this.education;
delete this.recognition;
delete this.reading;
delete this.writing;
delete this.interests;
delete this.skills;
return delete this.social;
}
/**
Get a safe count of the number of things in a section.
*/
count(obj) {
if (!obj) {
return 0;
}
if (obj.history) {
return obj.history.length;
}
if (obj.sets) {
return obj.sets.length;
}
return obj.length || 0;
}
add(moniker) {
var defSheet, newObject;
defSheet = FreshResume.default();
newObject = defSheet[moniker].history ? $.extend(true, {}, defSheet[moniker].history[0]) : moniker === 'skills' ? $.extend(true, {}, defSheet.skills.sets[0]) : $.extend(true, {}, defSheet[moniker][0]);
this[moniker] = this[moniker] || [];
if (this[moniker].history) {
this[moniker].history.push(newObject);
} else if (moniker === 'skills') {
this.skills.sets.push(newObject);
} else {
this[moniker].push(newObject);
}
return newObject;
}
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
hasProfile(socialNetwork) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.some(this.social, function(p) {
return p.network.trim().toLowerCase() === socialNetwork;
});
}
/** Return the specified network profile. */
getProfile(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.
*/
getProfiles(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. */
hasSkill(skill) {
skill = skill.trim().toLowerCase();
return this.skills && _.some(this.skills, function(sk) {
return sk.keywords && _.some(sk.keywords, function(kw) {
return kw.trim().toLowerCase() === skill;
});
});
}
/** Validate the sheet against the FRESH Resume schema. */
isValid(info) {
var ret, schemaObj, validate;
schemaObj = require('fresh-resume-schema');
validator = require('is-my-json-valid');
validate = validator(schemaObj, { // See Note [1].
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
ret = validate(this);
if (!ret) {
this.imp = this.imp || {};
this.imp.validationErrors = validate.errors;
}
return ret;
}
duration(unit) {
var inspector;
inspector = require('../inspectors/duration-inspector');
return inspector.run(this, 'employment.history', 'start', 'end', unit);
}
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
sort() {
var byDateDesc, sortSection;
byDateDesc = function(a, b) {
if (a.safe.start.isBefore(b.safe.start)) {
return 1;
} else {
if (a.safe.start.isAfter(b.safe.start)) {
return -1;
} else {
return 0;
}
}
};
sortSection = function(key) {
var ar, datedThings;
ar = __.get(this, key);
if (ar && ar.length) {
datedThings = obj.filter(function(o) {
return o.start;
});
return datedThings.sort(byDateDesc);
}
};
sortSection('employment.history');
sortSection('education.history');
sortSection('service.history');
sortSection('projects');
return this.writing && this.writing.sort(function(a, b) {
if (a.safe.date.isBefore(b.safe.date)) {
return 1;
} else {
return (a.safe.date.isAfter(b.safe.date) && -1) || 0;
}
});
}
};
/**
Get the default (starter) sheet.
*/
FreshResume.default = function() {
return new FreshResume().parseJSON(require('fresh-resume-starter').fresh);
};
/**
Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
along the way.
*/
FreshResume.stringify = function(obj) {
var replacer;
replacer = function(key, value) { // Exclude these keys from stringification
var exKeys;
exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'];
if (_.some(exKeys, function(val) {
return key.trim() === val;
})) {
return void 0;
} else {
return value;
}
};
return JSON.stringify(obj, replacer, 2);
};
_parseDates = function() {
var _fmt, replaceDatesInObject, that;
_fmt = require('./fluent-date').fmt;
that = this;
// TODO: refactor recursion
replaceDatesInObject = function(obj) {
if (!obj) {
return;
}
if (Object.prototype.toString.call(obj) === '[object Array]') {
obj.forEach(function(elem) {
return replaceDatesInObject(elem);
});
} else if (typeof obj === 'object') {
if (obj._isAMomentObject || obj.safe) {
return;
}
Object.keys(obj).forEach(function(key) {
return replaceDatesInObject(obj[key]);
});
['start', 'end', 'date'].forEach(function(val) {
if ((obj[val] !== void 0) && (!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');
}
}
});
}
};
Object.keys(this).forEach(function(member) {
replaceDatesInObject(that[member]);
});
};
/** Export the Sheet function/ctor. */
module.exports = FreshResume;
// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
// in addition to YYYY-MM-DD. The original regex:
// /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/
}).call(this);
//# sourceMappingURL=fresh-resume.js.map

View File

@ -1,263 +0,0 @@
(function() {
/**
Definition of the FRESHTheme class.
@module core/fresh-theme
@license MIT. See LICENSE.md for details.
*/
/* Load and parse theme source files. */
/* Load a single theme file. */
/* Return a more friendly name for certain formats. */
var EXTEND, FRESHTheme, FS, HMSTATUS, PATH, READFILES, _, _load, _loadOne, friendlyName, loadSafeJson, moment, parsePath, pathExists, validator;
FS = require('fs');
validator = require('is-my-json-valid');
_ = require('underscore');
PATH = require('path');
parsePath = require('parse-filepath');
pathExists = require('path-exists').sync;
EXTEND = require('extend');
HMSTATUS = require('./status-codes');
moment = require('moment');
loadSafeJson = require('../utils/safe-json-loader');
READFILES = require('recursive-readdir-sync');
/* A representation of a FRESH theme asset.
@class FRESHTheme */
FRESHTheme = class FRESHTheme {
constructor() {
this.baseFolder = 'src';
return;
}
/* Open and parse the specified theme. */
open(themeFolder) {
var cached, formatsHash, pathInfo, that, themeFile, themeInfo;
this.folder = themeFolder;
// Open the [theme-name].json file; should have the same name as folder
pathInfo = parsePath(themeFolder);
// Set up a formats hash for the theme
formatsHash = {};
// Load the theme
themeFile = PATH.join(themeFolder, 'theme.json');
themeInfo = loadSafeJson(themeFile);
if (themeInfo.ex) {
throw {
fluenterror: themeInfo.ex.op === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError
};
({
inner: themeInfo.ex.inner
});
}
that = this;
// Move properties from the theme JSON file to the theme object
EXTEND(true, this, themeInfo.json);
// Check for an "inherits" entry in the theme JSON.
if (this.inherits) {
cached = {};
_.each(this.inherits, function(th, key) {
var d, themePath, themesObj;
// First, see if this is one of the predefined FRESH themes. There are
// only a handful of these, but they may change over time, so we need to
// query the official source of truth: the fresh-themes repository, which
// mounts the themes conveniently by name to the module object, and which
// is embedded locally inside the HackMyResume installation.
// TODO: merge this code with
themesObj = require('fresh-themes');
if (_.has(themesObj.themes, th)) {
themePath = PATH.join(parsePath(require.resolve('fresh-themes')).dirname, '/themes/', th);
} else {
d = parsePath(th).dirname;
themePath = PATH.join(d, th);
}
cached[th] = cached[th] || new FRESHTheme().open(themePath);
return formatsHash[key] = cached[th].getFormat(key);
});
}
// Load theme files
formatsHash = _load.call(this, formatsHash);
// Cache
this.formats = formatsHash;
// Set the official theme name
this.name = parsePath(this.folder).name;
return this;
}
/* Determine if the theme supports the specified output format. */
hasFormat(fmt) {
return _.has(this.formats, fmt);
}
/* Determine if the theme supports the specified output format. */
getFormat(fmt) {
return this.formats[fmt];
}
};
_load = function(formatsHash) {
var copyOnly, fmts, jsFiles, major, that, tplFolder;
that = this;
major = false;
tplFolder = PATH.join(this.folder, this.baseFolder);
copyOnly = ['.ttf', '.otf', '.png', '.jpg', '.jpeg', '.pdf'];
// 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.
fmts = READFILES(tplFolder).map(function(absPath) {
return _loadOne.call(this, absPath, formatsHash, tplFolder);
}, this);
// Now, get all the CSS files...
this.cssFiles = fmts.filter(function(fmt) {
return fmt && (fmt.ext === 'css');
});
// For each CSS file, get its corresponding HTML file. It's possible that
// a theme can have a CSS file but *no* HTML file, as when a theme author
// creates a pure CSS override of an existing theme.
this.cssFiles.forEach(function(cssf) {
var idx;
idx = _.findIndex(fmts, function(fmt) {
return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
});
cssf.major = false;
if (idx > -1) {
fmts[idx].css = cssf.data;
return fmts[idx].cssPath = cssf.path;
} else {
if (that.inherits) {
// Found a CSS file without an HTML file in a theme that inherits
// from another theme. This is the override CSS file.
return that.overrides = {
file: cssf.path,
data: cssf.data
};
}
}
});
// Now, save all the javascript file paths to a theme property.
jsFiles = fmts.filter(function(fmt) {
return fmt && (fmt.ext === 'js');
});
this.jsFiles = jsFiles.map(function(jsf) {
return jsf['path'];
});
return formatsHash;
};
_loadOne = function(absPath, formatsHash, tplFolder) {
var absPathSafe, act, defFormats, idx, isPrimary, obj, outFmt, pathInfo, portion, ref, ref1, reg, res;
pathInfo = parsePath(absPath);
if (pathInfo.basename.toLowerCase() === 'theme.json') {
return;
}
absPathSafe = absPath.trim().toLowerCase();
outFmt = '';
act = 'copy';
isPrimary = false;
// If this is an "explicit" theme, all files of importance are specified in
// the "transform" section of the theme.json file.
if (this.explicit) {
outFmt = _.find(Object.keys(this.formats), function(fmtKey) {
var fmtVal;
fmtVal = this.formats[fmtKey];
return _.some(fmtVal.transform, function(fpath) {
var absPathB;
absPathB = PATH.join(this.folder, fpath).trim().toLowerCase();
return absPathB === absPathSafe;
}, this);
}, this);
if (outFmt) {
act = 'transform';
}
}
if (!outFmt) {
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the implicit output
// format for all files within the folder
portion = pathInfo.dirname.replace(tplFolder, '');
if (portion && portion.trim()) {
if (portion[1] === '_') {
return;
}
reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
res = reg.exec(portion);
if (res) {
if (res[1] !== 'partials') {
outFmt = res[1];
if (!this.explicit) {
act = 'transform';
}
} else {
this.partials = this.partials || [];
this.partials.push({
name: pathInfo.name,
path: absPath
});
return null;
}
}
}
}
if (!outFmt) {
idx = pathInfo.name.lastIndexOf('-');
outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx + 1);
if (!this.explicit) {
act = 'transform';
}
defFormats = require('./default-formats');
isPrimary = _.some(defFormats, function(form) {
return form.name === outFmt && pathInfo.extname !== '.css';
});
}
// Make sure we have a valid formatsHash
formatsHash[outFmt] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
};
// Move symlink descriptions from theme.json to the format
if ((ref = this.formats) != null ? (ref1 = ref[outFmt]) != null ? ref1.symLinks : void 0 : void 0) {
formatsHash[outFmt].symLinks = this.formats[outFmt].symLinks;
}
// Create the file representation object
obj = {
action: act,
primary: isPrimary,
path: absPath,
orgPath: PATH.relative(tplFolder, absPath),
ext: pathInfo.extname.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;
};
friendlyName = function(val) {
var friendly;
val = (val && val.trim().toLowerCase()) || '';
friendly = {
yml: 'yaml',
md: 'markdown',
txt: 'text'
};
return friendly[val] || val;
};
module.exports = FRESHTheme;
}).call(this);
//# sourceMappingURL=fresh-theme.js.map

View File

@ -1,342 +0,0 @@
(function() {
/**
Definition of the JRSResume class.
@license MIT. See LICENSE.md for details.
@module core/jrs-resume
*/
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
var CONVERTER, FS, JRSResume, MD, PATH, _, _parseDates, extend, moment, validator;
FS = require('fs');
extend = require('extend');
validator = require('is-my-json-valid');
_ = require('underscore');
PATH = require('path');
MD = require('marked');
CONVERTER = require('fresh-jrs-converter');
moment = require('moment');
JRSResume = (function() {
/** Reset the sheet to an empty state. */
var clear;
/**
A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
is an instantiation of that JSON decorated with utility methods.
@class JRSResume
*/
class JRSResume { // extends AbstractResume
/** Initialize the the JSResume from string. */
parse(stringData, opts) {
var ref;
this.imp = (ref = this.imp) != null ? ref : {
raw: stringData
};
return this.parseJSON(JSON.parse(stringData), opts);
}
/**
Initialize the JRSResume object from JSON.
Open and parse the specified JRS resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
*/
parseJSON(rep, opts) {
var ignoreList, privateList, ref, scrubbed, scrubber;
opts = opts || {};
if (opts.privatize) {
scrubber = require('../utils/resume-scrubber');
// Ignore any element with the 'ignore: true' or 'private: true' designator.
({scrubbed, ignoreList, privateList} = scrubber.scrubResume(rep, opts));
}
// Extend resume properties onto ourself.
extend(true, this, opts.privatize ? scrubbed : rep);
if (!((ref = this.imp) != null ? ref.processed : void 0)) {
// Set up metadata TODO: Clean up metadata on the object model.
opts = opts || {};
if (opts.imp === void 0 || opts.imp) {
this.imp = this.imp || {};
this.imp.title = (opts.title || this.imp.title) || this.basics.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
}
}
this.imp.processed = true;
}
// Parse dates, sort dates, and calculate computed values
(opts.date === void 0 || opts.date) && _parseDates.call(this);
(opts.sort === void 0 || opts.sort) && this.sort();
if (opts.compute === void 0 || opts.compute) {
this.basics.computed = {
numYears: this.duration(),
keywords: this.keywords()
};
}
return this;
}
/** Save the sheet to disk (for environments that have disk access). */
save(filename) {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(this), 'utf8');
return this;
}
/** Save the sheet to disk in a specific format, either FRESH or JRS. */
saveAs(filename, format) {
var newRep, stringRep;
if (format === 'JRS') {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
} else {
newRep = CONVERTER.toFRESH(this);
stringRep = CONVERTER.toSTRING(newRep);
FS.writeFileSync(filename, stringRep, 'utf8');
}
return this;
}
/** Return the resume format. */
format() {
return 'JRS';
}
stringify() {
return JRSResume.stringify(this);
}
/** Return a unique list of all keywords across all skills. */
keywords() {
var flatSkills;
flatSkills = [];
if (this.skills && this.skills.length) {
this.skills.forEach(function(s) {
return flatSkills = _.union(flatSkills, s.keywords);
});
}
return flatSkills;
}
/**
Return internal metadata. Create if it doesn't exist.
JSON Resume v0.0.0 doesn't allow additional properties at the root level,
so tuck this into the .basic sub-object.
*/
i() {
var ref;
return this.imp = (ref = this.imp) != null ? ref : {};
}
/** Add work experience to the sheet. */
add(moniker) {
var defSheet, newObject;
defSheet = JRSResume.default();
newObject = $.extend(true, {}, defSheet[moniker][0]);
this[moniker] = this[moniker] || [];
this[moniker].push(newObject);
return newObject;
}
/** Determine if the sheet includes a specific social profile (eg, GitHub). */
hasProfile(socialNetwork) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.basics.profiles && _.some(this.basics.profiles, function(p) {
return p.network.trim().toLowerCase() === socialNetwork;
});
}
/** Determine if the sheet includes a specific skill. */
hasSkill(skill) {
skill = skill.trim().toLowerCase();
return this.skills && _.some(this.skills, function(sk) {
return sk.keywords && _.some(sk.keywords, function(kw) {
return kw.trim().toLowerCase() === skill;
});
});
}
/** Validate the sheet against the JSON Resume schema. */
isValid() { // TODO: ↓ fix this path ↓
var ret, schema, schemaObj, temp, validate;
schema = FS.readFileSync(PATH.join(__dirname, 'resume.json'), 'utf8');
schemaObj = JSON.parse(schema);
validator = require('is-my-json-valid');
validate = validator(schemaObj, { // Note [1]
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
temp = this.imp;
delete this.imp;
ret = validate(this);
this.imp = temp;
if (!ret) {
this.imp = this.imp || {};
this.imp.validationErrors = validate.errors;
}
return ret;
}
duration(unit) {
var inspector;
inspector = require('../inspectors/duration-inspector');
return inspector.run(this, 'work', 'startDate', 'endDate', unit);
}
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
sort() {
var byDateDesc;
byDateDesc = function(a, b) {
if (a.safeStartDate.isBefore(b.safeStartDate)) {
return 1;
} else {
return (a.safeStartDate.isAfter(b.safeStartDate) && -1) || 0;
}
};
this.work && this.work.sort(byDateDesc);
this.education && this.education.sort(byDateDesc);
this.volunteer && this.volunteer.sort(byDateDesc);
this.awards && this.awards.sort(function(a, b) {
if (a.safeDate.isBefore(b.safeDate)) {
return 1;
} else {
return (a.safeDate.isAfter(b.safeDate) && -1) || 0;
}
});
return this.publications && this.publications.sort(function(a, b) {
if (a.safeReleaseDate.isBefore(b.safeReleaseDate)) {
return 1;
} else {
return (a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1) || 0;
}
});
}
dupe() {
var rnew;
rnew = new JRSResume();
rnew.parse(this.stringify(), {});
return rnew;
}
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
harden() {
var HD, HDIN, ret, transformer;
ret = this.dupe();
HD = function(txt) {
return '@@@@~' + txt + '~@@@@';
};
HDIN = function(txt) {
//return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
return HD(txt);
};
transformer = require('../utils/string-transformer');
return transformer(ret, ['skills', 'url', 'website', 'startDate', 'endDate', 'releaseDate', 'date', 'phone', 'email', 'address', 'postalCode', 'city', 'country', 'region', 'safeStartDate', 'safeEndDate'], function(key, val) {
return HD(val);
});
}
};
clear = function(clearMeta) {
clearMeta = ((clearMeta === void 0) && true) || clearMeta;
if (clearMeta) {
delete this.imp;
}
delete this.basics.computed; // Don't use Object.keys() here
delete this.work;
delete this.volunteer;
delete this.education;
delete this.awards;
delete this.publications;
delete this.interests;
delete this.skills;
return delete this.basics.profiles;
};
return JRSResume;
}).call(this);
/** Get the default (empty) sheet. */
JRSResume.default = function() {
return new JRSResume().parseJSON(require('fresh-resume-starter').jrs);
};
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
JRSResume.stringify = function(obj) {
var replacer;
replacer = function(key, value) { // Exclude these keys from stringification
var temp;
temp = _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'], function(val) {
return key.trim() === val;
});
if (temp) {
return void 0;
} else {
return value;
}
};
return JSON.stringify(obj, replacer, 2);
};
_parseDates = function() {
var _fmt;
_fmt = require('./fluent-date').fmt;
this.work && this.work.forEach(function(job) {
job.safeStartDate = _fmt(job.startDate);
return job.safeEndDate = _fmt(job.endDate);
});
this.education && this.education.forEach(function(edu) {
edu.safeStartDate = _fmt(edu.startDate);
return edu.safeEndDate = _fmt(edu.endDate);
});
this.volunteer && this.volunteer.forEach(function(vol) {
vol.safeStartDate = _fmt(vol.startDate);
return vol.safeEndDate = _fmt(vol.endDate);
});
this.awards && this.awards.forEach(function(awd) {
return awd.safeDate = _fmt(awd.date);
});
return this.publications && this.publications.forEach(function(pub) {
return pub.safeReleaseDate = _fmt(pub.releaseDate);
});
};
/**
Export the JRSResume function/ctor.
*/
module.exports = JRSResume;
}).call(this);
//# sourceMappingURL=jrs-resume.js.map

View File

@ -1,99 +0,0 @@
(function() {
/**
Definition of the JRSTheme class.
@module core/jrs-theme
@license MIT. See LICENSE.MD for details.
*/
var JRSTheme, PATH, _, errors, parsePath, pathExists;
_ = require('underscore');
PATH = require('path');
parsePath = require('parse-filepath');
pathExists = require('path-exists').sync;
errors = require('./status-codes');
/**
The JRSTheme class is a representation of a JSON Resume theme asset.
@class JRSTheme
*/
JRSTheme = class JRSTheme {
/**
Open and parse the specified JRS theme.
@method open
*/
open(thFolder) {
var pathInfo, pkgJsonPath, thApi, thPkg;
this.folder = thFolder;
pathInfo = parsePath(thFolder);
// Open and parse the theme's package.json file
pkgJsonPath = PATH.join(thFolder, 'package.json');
if (pathExists(pkgJsonPath)) {
thApi = require(thFolder); // Requiring the folder yields whatever the package.json's "main" is set to
thPkg = require(pkgJsonPath); // Get the package.json as JSON
this.name = thPkg.name;
this.render = (thApi && thApi.render) || void 0;
this.engine = 'jrs';
// Create theme formats (HTML and PDF). Just add the bare minimum mix of
// properties necessary to allow JSON Resume themes to share a rendering
// path with FRESH themes.
this.formats = {
html: {
outFormat: 'html',
files: [
{
action: 'transform',
render: this.render,
primary: true,
ext: 'html',
css: null
}
]
},
pdf: {
outFormat: 'pdf',
files: [
{
action: 'transform',
render: this.render,
primary: true,
ext: 'pdf',
css: null
}
]
}
};
} else {
throw {
fluenterror: errors.missingPackageJSON
};
}
return this;
}
/**
Determine if the theme supports the output format.
@method hasFormat
*/
hasFormat(fmt) {
return _.has(this.formats, fmt);
}
/**
Return the requested output format.
@method getFormat
*/
getFormat(fmt) {
return this.formats[fmt];
}
};
module.exports = JRSTheme;
}).call(this);
//# sourceMappingURL=jrs-theme.js.map

View File

@ -1,132 +0,0 @@
(function() {
/**
Definition of the ResumeFactory class.
@license MIT. See LICENSE.md for details.
@module core/resume-factory
*/
/**
A simple factory class for FRESH and JSON Resumes.
@class ResumeFactory
*/
var FS, HME, HMS, ResumeConverter, ResumeFactory, SyntaxErrorEx, _, _parse, chalk, resumeDetect;
FS = require('fs');
HMS = require('./status-codes');
HME = require('./event-codes');
ResumeConverter = require('fresh-jrs-converter');
chalk = require('chalk');
SyntaxErrorEx = require('../utils/syntax-error-ex');
_ = require('underscore');
resumeDetect = require('../utils/resume-detector');
require('string.prototype.startswith');
ResumeFactory = module.exports = {
/**
Load one or more resumes from disk.
@param {Object} opts An options object with settings for the factory as well
as passthrough settings for FRESHResume or JRSResume. Structure:
{
format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null)
objectify: true, // FRESH/JRSResume or raw JSON?
inner: { // Passthru options for FRESH/JRSResume
sort: false
}
}
*/
load: function(sources, opts, emitter) {
return sources.map(function(src) {
return this.loadOne(src, opts, emitter);
}, this);
},
/** Load a single resume from disk. */
loadOne: function(src, opts, emitter) {
var ResumeClass, info, json, orgFormat, reqLib, rez, toFormat;
toFormat = opts.format; // Can be null
// Get the destination format. Can be 'fresh', 'jrs', or null/undefined.
toFormat && (toFormat = toFormat.toLowerCase().trim());
// Load and parse the resume JSON
info = _parse(src, opts, emitter);
if (info.fluenterror) {
return info;
}
// Determine the resume format: FRESH or JRS
json = info.json;
orgFormat = resumeDetect(json);
if (orgFormat === 'unk') {
info.fluenterror = HMS.unknownSchema;
return info;
}
// Convert between formats if necessary
if (toFormat && (orgFormat !== toFormat)) {
json = ResumeConverter['to' + toFormat.toUpperCase()](json);
}
// Objectify the resume, that is, convert it from JSON to a FRESHResume
// or JRSResume object.
rez = null;
if (opts.objectify) {
reqLib = '../core/' + (toFormat || orgFormat) + '-resume';
ResumeClass = require(reqLib);
rez = new ResumeClass().parseJSON(json, opts.inner);
rez.i().file = src;
}
return {
file: src,
json: info.json,
rez: rez
};
}
};
_parse = function(fileName, opts, eve) {
var err, orgFormat, rawData, ret;
rawData = null;
try {
// Read the file
eve && eve.stat(HME.beforeRead, {
file: fileName
});
rawData = FS.readFileSync(fileName, 'utf8');
eve && eve.stat(HME.afterRead, {
file: fileName,
data: rawData
});
eve && eve.stat(HME.beforeParse, {
data: rawData
});
ret = {
json: JSON.parse(rawData)
};
orgFormat = ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@') ? 'fresh' : 'jrs';
eve && eve.stat(HME.afterParse, {
file: fileName,
data: ret.json,
fmt: orgFormat
});
return ret;
} catch (error) {
err = error;
return {
// Can be ENOENT, EACCES, SyntaxError, etc.
fluenterror: rawData ? HMS.parseError : HMS.readError,
inner: err,
raw: rawData,
file: fileName
};
}
};
}).call(this);
//# sourceMappingURL=resume-factory.js.map

380
dist/core/resume.json vendored
View File

@ -1,380 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Resume Schema",
"type": "object",
"additionalProperties": false,
"properties": {
"basics": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string"
},
"label": {
"type": "string",
"description": "e.g. Web Developer"
},
"picture": {
"type": "string",
"description": "URL (as per RFC 3986) to a picture in JPEG or PNG format"
},
"email": {
"type": "string",
"description": "e.g. thomas@gmail.com",
"format": "email"
},
"phone": {
"type": "string",
"description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923"
},
"website": {
"type": "string",
"description": "URL (as per RFC 3986) to your website, e.g. personal homepage",
"format": "uri"
},
"summary": {
"type": "string",
"description": "Write a short 2-3 sentence biography about yourself"
},
"location": {
"type": "object",
"additionalProperties": true,
"properties": {
"address": {
"type": "string",
"description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li."
},
"postalCode": {
"type": "string"
},
"city": {
"type": "string"
},
"countryCode": {
"type": "string",
"description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN"
},
"region": {
"type": "string",
"description": "The general region where you live. Can be a US state, or a province, for instance."
}
}
},
"profiles": {
"type": "array",
"description": "Specify any number of social networks that you participate in",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"network": {
"type": "string",
"description": "e.g. Facebook or Twitter"
},
"username": {
"type": "string",
"description": "e.g. neutralthoughts"
},
"url": {
"type": "string",
"description": "e.g. http://twitter.com/neutralthoughts"
}
}
}
}
}
},
"work": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"company": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"volunteer": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"organization": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"education": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"institution": {
"type": "string",
"description": "e.g. Massachusetts Institute of Technology"
},
"area": {
"type": "string",
"description": "e.g. Arts"
},
"studyType": {
"type": "string",
"description": "e.g. Bachelor"
},
"startDate": {
"type": "string",
"description": "e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"gpa": {
"type": "string",
"description": "grade point average, e.g. 3.67/4.0"
},
"courses": {
"type": "array",
"description": "List notable courses/subjects",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. H1302 - Introduction to American history"
}
}
}
}
},
"awards": {
"type": "array",
"description": "Specify any awards you have received throughout your professional career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"title": {
"type": "string",
"description": "e.g. One of the 100 greatest minds of the century"
},
"date": {
"type": "string",
"description": "e.g. 1989-06-12",
"format": "date"
},
"awarder": {
"type": "string",
"description": "e.g. Time Magazine"
},
"summary": {
"type": "string",
"description": "e.g. Received for my work with Quantum Physics"
}
}
}
},
"publications": {
"type": "array",
"description": "Specify your publications through your career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. The World Wide Web"
},
"publisher": {
"type": "string",
"description": "e.g. IEEE, Computer Magazine"
},
"releaseDate": {
"type": "string",
"description": "e.g. 1990-08-01"
},
"website": {
"type": "string",
"description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html"
},
"summary": {
"type": "string",
"description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML."
}
}
}
},
"skills": {
"type": "array",
"description": "List out your professional skill-set",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Web Development"
},
"level": {
"type": "string",
"description": "e.g. Master"
},
"keywords": {
"type": "array",
"description": "List some keywords pertaining to this skill",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. HTML"
}
}
}
}
},
"languages": {
"type": "array",
"description": "List any other languages you speak",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"language": {
"type": "string",
"description": "e.g. English, Spanish"
},
"fluency": {
"type": "string",
"description": "e.g. Fluent, Beginner"
}
}
}
},
"interests": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Philosophy"
},
"keywords": {
"type": "array",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Friedrich Nietzsche"
}
}
}
}
},
"references": {
"type": "array",
"description": "List references you have received",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Timothy Cook"
},
"reference": {
"type": "string",
"description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing."
}
}
}
}
}
}

View File

@ -1,44 +0,0 @@
(function() {
/**
Status codes for HackMyResume.
@module core/status-codes
@license MIT. See LICENSE.MD for details.
*/
module.exports = {
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3,
missingCommand: 4,
invalidCommand: 5,
resumeNotFoundAlt: 6,
inputOutputParity: 7,
createNameMissing: 8,
pdfGeneration: 9,
missingPackageJSON: 10,
invalid: 11,
invalidFormat: 12,
notOnPath: 13,
readError: 14,
parseError: 15,
fileSaveError: 16,
generateError: 17,
invalidHelperUse: 18,
mixedMerge: 19,
invokeTemplate: 20,
compileTemplate: 21,
themeLoad: 22,
invalidParamCount: 23,
missingParam: 24,
createError: 25,
validateError: 26,
invalidOptionsFile: 27,
optionsFileNotFound: 28,
unknownSchema: 29,
themeHelperLoad: 30,
invalidSchemaVersion: 31
};
}).call(this);
//# sourceMappingURL=status-codes.js.map

View File

@ -1,34 +0,0 @@
(function() {
/**
Definition of the BaseGenerator class.
@module generators/base-generator
@license MIT. See LICENSE.md for details.
*/
var BaseGenerator;
/**
The BaseGenerator class is the root of the generator hierarchy. Functionality
common to ALL generators lives here.
*/
module.exports = BaseGenerator = (function() {
class BaseGenerator {
/** Base-class initialize. */
constructor(format) {
this.format = format;
}
};
/** Status codes. */
BaseGenerator.prototype.codes = require('../core/status-codes');
/** Generator options. */
BaseGenerator.prototype.opts = {};
return BaseGenerator;
}).call(this);
}).call(this);
//# sourceMappingURL=base-generator.js.map

View File

@ -1,43 +0,0 @@
(function() {
/**
Definition of the HTMLGenerator class.
@module generators/html-generator
@license MIT. See LICENSE.md for details.
*/
var FS, HTML, HtmlGenerator, PATH, TemplateGenerator;
TemplateGenerator = require('./template-generator');
FS = require('fs-extra');
HTML = require('html');
PATH = require('path');
require('string.prototype.endswith');
module.exports = HtmlGenerator = class HtmlGenerator extends TemplateGenerator {
constructor() {
super('html');
}
/**
Copy satellite CSS files to the destination and optionally pretty-print
the HTML resume prior to saving.
*/
onBeforeSave(info) {
if (info.outputFile.endsWith('.css')) {
return info.mk;
}
if (this.opts.prettify) {
return HTML.prettyPrint(info.mk, this.opts.prettify);
} else {
return info.mk;
}
}
};
}).call(this);
//# sourceMappingURL=html-generator.js.map

View File

@ -1,126 +0,0 @@
(function() {
/**
Definition of the HtmlPdfCLIGenerator class.
@module generators/html-pdf-generator.js
@license MIT. See LICENSE.md for details.
*/
var FS, HMSTATUS, HtmlPdfCLIGenerator, PATH, SLASH, SPAWN, TemplateGenerator, _, engines;
TemplateGenerator = require('./template-generator');
FS = require('fs-extra');
PATH = require('path');
SLASH = require('slash');
_ = require('underscore');
HMSTATUS = require('../core/status-codes');
SPAWN = require('../utils/safe-spawn');
/**
An HTML-driven PDF resume generator for HackMyResume. Talks to Phantom,
wkhtmltopdf, and other PDF engines over a CLI (command-line interface).
If an engine isn't installed for a particular platform, error out gracefully.
*/
module.exports = HtmlPdfCLIGenerator = class HtmlPdfCLIGenerator extends TemplateGenerator {
constructor() {
super('pdf', 'html');
}
/** Generate the binary PDF. */
onBeforeSave(info) {
var safe_eng;
if (info.ext !== 'html' && info.ext !== 'pdf') {
//console.dir _.omit( info, 'mk' ), depth: null, colors: true
return info.mk;
}
safe_eng = info.opts.pdf || 'wkhtmltopdf';
if (safe_eng === 'phantom') {
safe_eng = 'phantomjs';
}
if (_.has(engines, safe_eng)) {
this.errHandler = info.opts.errHandler;
engines[safe_eng].call(this, info.mk, info.outputFile, info.opts, this.onError);
return null; // halt further processing
}
}
/* Low-level error callback for spawn(). May be called after HMR process
termination, so object references may not be valid here. That's okay; if
the references are invalid, the error was already logged. We could use
spawn-watch here but that causes issues on legacy Node.js. */
onError(ex, param) {
var ref;
if ((ref = param.errHandler) != null) {
if (typeof ref.err === "function") {
ref.err(HMSTATUS.pdfGeneration, ex);
}
}
}
};
// TODO: Move each engine to a separate module
engines = {
/**
Generate a PDF from HTML using wkhtmltopdf's CLI interface.
Spawns a child process with `wkhtmltopdf <source> <target>`. wkhtmltopdf
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease wkhtmltopdf rendering
*/
wkhtmltopdf: function(markup, fOut, opts, on_error) {
var tempFile, wkargs, wkopts;
// Save the markup to a temporary file
tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
// Prepare wkhtmltopdf arguments.
wkopts = _.extend({
'margin-top': '10mm',
'margin-bottom': '10mm'
}, opts.wkhtmltopdf);
wkopts = _.flatten(_.map(wkopts, function(v, k) {
return ['--' + k, v];
}));
wkargs = wkopts.concat([tempFile, fOut]);
SPAWN('wkhtmltopdf', wkargs, false, on_error, this);
},
/**
Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
*/
phantomjs: function(markup, fOut, opts, on_error) {
var destPath, scriptPath, sourcePath, tempFile;
// Save the markup to a temporary file
tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
scriptPath = PATH.relative(process.cwd(), PATH.resolve(__dirname, '../utils/rasterize.js'));
scriptPath = SLASH(scriptPath);
sourcePath = SLASH(PATH.relative(process.cwd(), tempFile));
destPath = SLASH(PATH.relative(process.cwd(), fOut));
SPAWN('phantomjs', [scriptPath, sourcePath, destPath], false, on_error, this);
},
/**
Generate a PDF from HTML using WeasyPrint's CLI interface.
Spawns a child process with `weasyprint <source> <target>`. Weasy Print
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
*/
weasyprint: function(markup, fOut, opts, on_error) {
var tempFile;
// Save the markup to a temporary file
tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
SPAWN('weasyprint', [tempFile, fOut], false, on_error, this);
}
};
}).call(this);
//# sourceMappingURL=html-pdf-cli-generator.js.map

View File

@ -1,65 +0,0 @@
(function() {
/**
Definition of the HtmlPngGenerator class.
@module generators/html-png-generator
@license MIT. See LICENSE.MD for details.
*/
/**
Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
*/
var FS, HTML, HtmlPngGenerator, PATH, SLASH, SPAWN, TemplateGenerator, phantom;
TemplateGenerator = require('./template-generator');
FS = require('fs-extra');
HTML = require('html');
SLASH = require('slash');
SPAWN = require('../utils/safe-spawn');
PATH = require('path');
/**
An HTML-based PNG resume generator for HackMyResume.
*/
module.exports = HtmlPngGenerator = class HtmlPngGenerator extends TemplateGenerator {
constructor() {
super('png', 'html');
}
invoke(rez, themeMarkup, cssInfo, opts) {}
// TODO: Not currently called or callable.
generate(rez, f, opts) {
var htmlFile, htmlResults;
htmlResults = opts.targets.filter(function(t) {
return t.fmt.outFormat === 'html';
});
htmlFile = htmlResults[0].final.files.filter(function(fl) {
return fl.info.ext === 'html';
});
phantom(htmlFile[0].data, f);
}
};
phantom = function(markup, fOut) {
var destPath, info, scriptPath, sourcePath, tempFile;
// Save the markup to a temporary file
tempFile = fOut.replace(/\.png$/i, '.png.html');
FS.writeFileSync(tempFile, markup, 'utf8');
scriptPath = SLASH(PATH.relative(process.cwd(), PATH.resolve(__dirname, '../utils/rasterize.js')));
sourcePath = SLASH(PATH.relative(process.cwd(), tempFile));
destPath = SLASH(PATH.relative(process.cwd(), fOut));
info = SPAWN('phantomjs', [scriptPath, sourcePath, destPath]);
};
}).call(this);
//# sourceMappingURL=html-png-generator.js.map

View File

@ -1,38 +0,0 @@
(function() {
/**
Definition of the JsonGenerator class.
@module generators/json-generator
@license MIT. See LICENSE.md for details.
*/
var BaseGenerator, FJCV, FS, JsonGenerator, _;
BaseGenerator = require('./base-generator');
FS = require('fs');
_ = require('underscore');
FJCV = require('fresh-jrs-converter');
/** The JsonGenerator generates a FRESH or JRS resume as an output. */
module.exports = JsonGenerator = class JsonGenerator extends BaseGenerator {
constructor() {
super('json');
}
invoke(rez) {
var altRez;
altRez = FJCV['to' + (rez.format() === 'FRESH' ? 'JRS' : 'FRESH')](rez);
return altRez = FJCV.toSTRING(altRez);
}
//altRez.stringify()
generate(rez, f) {
FS.writeFileSync(f, this.invoke(rez), 'utf8');
}
};
}).call(this);
//# sourceMappingURL=json-generator.js.map

View File

@ -1,40 +0,0 @@
(function() {
/**
Definition of the JsonYamlGenerator class.
@module generators/json-yaml-generator
@license MIT. See LICENSE.md for details.
*/
var BaseGenerator, FS, JsonYamlGenerator, YAML;
BaseGenerator = require('./base-generator');
FS = require('fs');
YAML = require('yamljs');
/**
JsonYamlGenerator takes a JSON resume object and translates it directly to
JSON without a template, producing an equivalent YAML-formatted resume. See
also YamlGenerator (yaml-generator.js).
*/
module.exports = JsonYamlGenerator = class JsonYamlGenerator extends BaseGenerator {
constructor() {
super('yml');
}
invoke(rez, themeMarkup, cssInfo, opts) {
return YAML.stringify(JSON.parse(rez.stringify()), 2e308, 2);
}
generate(rez, f, opts) {
var data;
data = YAML.stringify(JSON.parse(rez.stringify()), 2e308, 2);
FS.writeFileSync(f, data, 'utf8');
return data;
}
};
}).call(this);
//# sourceMappingURL=json-yaml-generator.js.map

View File

@ -1,23 +0,0 @@
(function() {
/**
Definition of the LaTeXGenerator class.
@module generators/latex-generator
@license MIT. See LICENSE.md for details.
*/
var LaTeXGenerator, TemplateGenerator;
TemplateGenerator = require('./template-generator');
/**
LaTeXGenerator generates a LaTeX resume via TemplateGenerator.
*/
module.exports = LaTeXGenerator = class LaTeXGenerator extends TemplateGenerator {
constructor() {
super('latex', 'tex');
}
};
}).call(this);
//# sourceMappingURL=latex-generator.js.map

View File

@ -1,23 +0,0 @@
(function() {
/**
Definition of the MarkdownGenerator class.
@module generators/markdown-generator
@license MIT. See LICENSE.md for details.
*/
var MarkdownGenerator, TemplateGenerator;
TemplateGenerator = require('./template-generator');
/**
MarkdownGenerator generates a Markdown-formatted resume via TemplateGenerator.
*/
module.exports = MarkdownGenerator = class MarkdownGenerator extends TemplateGenerator {
constructor() {
super('md', 'txt');
}
};
}).call(this);
//# sourceMappingURL=markdown-generator.js.map

View File

@ -1,278 +0,0 @@
(function() {
/**
Definition of the TemplateGenerator class. TODO: Refactor
@module generators/template-generator
@license MIT. See LICENSE.md for details.
*/
/** Default template generator options. */
/** Freeze newlines for protection against errant JST parsers. */
/** Unfreeze newlines when the coast is clear. */
var BaseGenerator, EXTEND, FRESHTheme, FS, JRSTheme, MD, MKDIRP, PATH, TemplateGenerator, XML, _, _defaultOpts, _reg, createSymLinks, freeze, parsePath, unfreeze;
FS = require('fs-extra');
_ = require('underscore');
MD = require('marked');
XML = require('xml-escape');
PATH = require('path');
parsePath = require('parse-filepath');
MKDIRP = require('mkdirp');
BaseGenerator = require('./base-generator');
EXTEND = require('extend');
FRESHTheme = require('../core/fresh-theme');
JRSTheme = require('../core/jrs-theme');
/**
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
*/
module.exports = TemplateGenerator = class TemplateGenerator extends BaseGenerator {
/** Constructor. Set the output format and template format for this
generator. Will usually be called by a derived generator such as
HTMLGenerator or MarkdownGenerator. */
constructor(outputFormat, templateFormat, cssFile) {
super(outputFormat);
this.tplFormat = templateFormat || outputFormat;
return;
}
/** Generate a resume using string-based inputs and outputs without touching
the filesystem.
@method invoke
@param rez A FreshResume object.
@param opts Generator options.
@returns {Array} An array of objects representing the generated output
files. */
invoke(rez, opts) {
var curFmt, results;
opts = opts ? (this.opts = EXTEND(true, {}, _defaultOpts, opts)) : this.opts;
// Sort such that CSS files are processed before others
curFmt = opts.themeObj.getFormat(this.format);
curFmt.files = _.sortBy(curFmt.files, function(fi) {
return fi.ext !== 'css';
});
// Run the transformation!
results = curFmt.files.map(function(tplInfo, idx) {
var trx;
if (tplInfo.action === 'transform') {
trx = this.transform(rez, tplInfo.data, this.format, opts, opts.themeObj, curFmt);
if (tplInfo.ext === 'css') {
curFmt.files[idx].data = trx;
} else {
tplInfo.ext === 'html';
}
} else {
}
if (typeof opts.onTransform === "function") {
opts.onTransform(tplInfo);
}
return {
info: tplInfo,
data: trx
};
}, this);
return {
files: results
};
}
/** Generate a resume using file-based inputs and outputs. Requires access
to the local filesystem.
@method generate
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options. */
generate(rez, f, opts) {
var curFmt, genInfo, outFolder;
// Prepare
this.opts = EXTEND(true, {}, _defaultOpts, opts);
// Call the string-based generation method
genInfo = this.invoke(rez, null);
outFolder = parsePath(f).dirname;
curFmt = opts.themeObj.getFormat(this.format);
// Process individual files within this format. For example, the HTML
// output format for a theme may have multiple HTML files, CSS files,
// etc. Process them here.
genInfo.files.forEach(function(file) {
var thisFilePath;
// console.dir _.omit(file.info,'cssData','data','css' )
// Pre-processing
file.info.orgPath = file.info.orgPath || '';
thisFilePath = file.info.primary ? f : PATH.join(outFolder, file.info.orgPath);
if (file.info.action !== 'copy' && this.onBeforeSave) {
file.data = this.onBeforeSave({
theme: opts.themeObj,
outputFile: thisFilePath,
mk: file.data,
opts: this.opts,
ext: file.info.ext
});
if (!file.data) {
return;
}
}
if (typeof opts.beforeWrite === "function") {
opts.beforeWrite({
data: thisFilePath
});
}
MKDIRP.sync(PATH.dirname(thisFilePath));
if (file.info.action !== 'copy') {
FS.writeFileSync(thisFilePath, file.data, {
encoding: 'utf8',
flags: 'w'
});
} else {
FS.copySync(file.info.path, thisFilePath);
}
if (typeof opts.afterWrite === "function") {
opts.afterWrite({
data: thisFilePath
});
}
// Post-processing
if (this.onAfterSave) {
return this.onAfterSave({
outputFile: fileName,
mk: file.data,
opts: this.opts
});
}
}, this);
// Some themes require a symlink structure. If so, create it.
createSymLinks(curFmt, outFolder);
return genInfo;
}
/** Perform a single resume resume transformation using string-based inputs
and outputs without touching the local file system.
@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. */
transform(json, jst, format, opts, theme, curFmt) {
var eng, result;
if (this.opts.freezeBreaks) {
jst = freeze(jst);
}
eng = require('../renderers/' + theme.engine + '-generator');
result = eng.generate(json, jst, format, curFmt, opts, theme);
if (this.opts.freezeBreaks) {
result = unfreeze(result);
}
return result;
}
};
createSymLinks = function(curFmt, outFolder) {
// Some themes require a symlink structure. If so, create it.
if (curFmt.symLinks) {
Object.keys(curFmt.symLinks).forEach(function(loc) {
var absLoc, absTarg, err, succeeded, type;
absLoc = PATH.join(outFolder, loc);
absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
// Set type to 'file', 'dir', or 'junction' (Windows only)
type = parsePath(absLoc).extname ? 'file' : 'junction';
try {
return FS.symlinkSync(absTarg, absLoc, type);
} catch (error) {
err = error;
succeeded = false;
if (err.code === 'EEXIST') {
FS.unlinkSync(absLoc);
try {
FS.symlinkSync(absTarg, absLoc, type);
succeeded = true;
} catch (error) {}
}
if (!succeeded) {
throw ex;
}
}
});
}
};
freeze = function(markup) {
markup.replace(_reg.regN, _defaultOpts.nSym);
return markup.replace(_reg.regR, _defaultOpts.rSym);
};
unfreeze = function(markup) {
markup.replace(_reg.regSymR, '\r');
return markup.replace(_reg.regSymN, '\n');
};
_defaultOpts = {
engine: 'underscore',
keepBreaks: true,
freezeBreaks: false,
nSym: '&newl;', // newline entity
rSym: '&retn;', // return entity
template: {
interpolate: /\{\{(.+?)\}\}/g,
escape: /\{\{\=(.+?)\}\}/g,
evaluate: /\{\%(.+?)\%\}/g,
comment: /\{\#(.+?)\#\}/g
},
filters: {
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, '');
},
lower: function(txt) {
return txt.toLowerCase();
},
link: function(name, url) {
if (url) {
return '<a href="' + url + '">' + name + '</a>';
} else {
return name;
}
}
},
prettify: {
indent_size: 2,
unformatted: ['em', 'strong', 'a'],
max_char: 80 // ← See lib/html.js in above-linked repo
}
};
//wrap_line_length: 120, <-- Don't use this
/** Regexes for linebreak preservation. */
_reg = {
regN: new RegExp('\n', 'g'),
regR: new RegExp('\r', 'g'),
regSymN: new RegExp(_defaultOpts.nSym, 'g'),
regSymR: new RegExp(_defaultOpts.rSym, 'g')
};
}).call(this);
//# sourceMappingURL=template-generator.js.map

View File

@ -1,23 +0,0 @@
(function() {
/**
Definition of the TextGenerator class.
@module generators/text-generator
@license MIT. See LICENSE.md for details.
*/
var TemplateGenerator, TextGenerator;
TemplateGenerator = require('./template-generator');
/**
The TextGenerator generates a plain-text resume via the TemplateGenerator.
*/
module.exports = TextGenerator = class TextGenerator extends TemplateGenerator {
constructor() {
super('txt');
}
};
}).call(this);
//# sourceMappingURL=text-generator.js.map

View File

@ -1,20 +0,0 @@
(function() {
/*
Definition of the WordGenerator class.
@module generators/word-generator
@license MIT. See LICENSE.md for details.
*/
var TemplateGenerator, WordGenerator;
TemplateGenerator = require('./template-generator');
module.exports = WordGenerator = class WordGenerator extends TemplateGenerator {
constructor() {
super('doc', 'xml');
}
};
}).call(this);
//# sourceMappingURL=word-generator.js.map

View File

@ -1,21 +0,0 @@
(function() {
/**
Definition of the XMLGenerator class.
@license MIT. See LICENSE.md for details.
@module generatprs/xml-generator
*/
var BaseGenerator, XMLGenerator;
BaseGenerator = require('./base-generator');
/** The XmlGenerator generates an XML resume via the TemplateGenerator. */
module.exports = XMLGenerator = class XMLGenerator extends BaseGenerator {
constructor() {
super('xml');
}
};
}).call(this);
//# sourceMappingURL=xml-generator.js.map

View File

@ -1,23 +0,0 @@
(function() {
/**
Definition of the YAMLGenerator class.
@module yaml-generator.js
@license MIT. See LICENSE.md for details.
*/
var TemplateGenerator, YAMLGenerator;
TemplateGenerator = require('./template-generator');
/**
YamlGenerator generates a YAML-formatted resume via TemplateGenerator.
*/
module.exports = YAMLGenerator = class YAMLGenerator extends TemplateGenerator {
constructor() {
super('yml', 'yml');
}
};
}).call(this);
//# sourceMappingURL=yaml-generator.js.map

View File

@ -1,74 +0,0 @@
(function() {
/**
Block helper definitions for HackMyResume / FluentCV.
@license MIT. See LICENSE.md for details.
@module helpers/generic-helpers
*/
/** Block helper function definitions. */
var BlockHelpers, HMSTATUS, LO, _, unused;
HMSTATUS = require('../core/status-codes');
LO = require('lodash');
_ = require('underscore');
unused = require('../utils/string');
BlockHelpers = module.exports = {
/**
Emit the enclosed content if the resume has a section with
the specified name. Otherwise, emit an empty string ''.
*/
section: function(title, options) {
var obj, ret;
title = title.trim().toLowerCase();
obj = LO.get(this.r, title);
ret = '';
if (obj) {
if (_.isArray(obj)) {
if (obj.length) {
ret = options.fn(this);
}
} else if (_.isObject(obj)) {
if ((obj.history && obj.history.length) || (obj.sets && obj.sets.length)) {
ret = options.fn(this);
}
}
}
return ret;
},
ifHasSkill: function(rez, skill, options) {
var ret, skUp;
skUp = skill.toUpperCase();
ret = _.some(rez.skills.list, function(sk) {
return (skUp.toUpperCase() === sk.name.toUpperCase()) && sk.years;
}, this);
if (ret) {
return options.fn(this);
}
},
/**
Emit the enclosed content if the resume has the named
property or subproperty.
*/
has: function(title, options) {
title = title && title.trim().toLowerCase();
if (LO.get(this.r, title)) {
return options.fn(this);
}
},
/**
Return true if either value is truthy.
@method either
*/
either: function(lhs, rhs, options) {
if (lhs || rhs) {
return options.fn(this);
}
}
};
}).call(this);
//# sourceMappingURL=block-helpers.js.map

View File

@ -1,64 +0,0 @@
(function() {
/**
Generic template helper definitions for command-line output.
@module console-helpers.js
@license MIT. See LICENSE.md for details.
*/
var CHALK, LO, PAD, _, consoleFormatHelpers;
PAD = require('string-padding');
LO = require('lodash');
CHALK = require('chalk');
_ = require('underscore');
require('../utils/string');
consoleFormatHelpers = module.exports = {
v: function(val, defaultVal, padding, style) {
var retVal, spaces;
retVal = (val === null || val === void 0) ? defaultVal : val;
spaces = 0;
if (String.is(padding)) {
spaces = parseInt(padding, 10);
if (isNaN(spaces)) {
spaces = 0;
}
} else if (_.isNumber(padding)) {
spaces = padding;
}
if (spaces !== 0) {
retVal = PAD(retVal, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT);
}
if (style && String.is(style)) {
retVal = LO.get(CHALK, style)(retVal);
}
return retVal;
},
gapLength: function(val) {
if (val < 35) {
return CHALK.green.bold(val);
} else if (val < 95) {
return CHALK.yellow.bold(val);
} else {
return CHALK.red.bold(val);
}
},
style: function(val, style) {
return LO.get(CHALK, style)(val);
},
isPlural: function(val, options) {
if (val > 1) {
return options.fn(this);
}
},
pad: function(val, spaces) {
return PAD(val, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT);
}
};
}).call(this);
//# sourceMappingURL=console-helpers.js.map

View File

@ -1,687 +0,0 @@
(function() {
/**
Generic template helper definitions for HackMyResume / FluentCV.
@license MIT. See LICENSE.md for details.
@module helpers/generic-helpers
*/
/** Generic template helper function definitions. */
/**
Format a from/to date range for display.
*/
/**
Report an error to the outside world without throwing an exception. Currently
relies on kludging the running verb into. opts.
*/
var FS, FluentDate, GenericHelpers, H2W, HMSTATUS, LO, MD, PATH, XML, _, _fromTo, _reportError, _skillLevelToIndex, moment, printf, unused;
MD = require('marked');
H2W = require('../utils/html-to-wpml');
XML = require('xml-escape');
FluentDate = require('../core/fluent-date');
HMSTATUS = require('../core/status-codes');
moment = require('moment');
FS = require('fs');
LO = require('lodash');
PATH = require('path');
printf = require('printf');
_ = require('underscore');
unused = require('../utils/string');
GenericHelpers = module.exports = {
/**
Emit a formatted string representing the specified datetime.
Convert the input date to the specified format through Moment.js. If date is
valid, return the formatted date string. If date is null, undefined, or other
falsy value, return the value of the 'fallback' parameter, if specified, or
null if no fallback was specified. If date is invalid, but not null/undefined/
falsy, return it as-is.
@param {string|Moment} datetime A date value.
@param {string} [dtFormat='YYYY-MM'] The desired datetime format. Must be a
Moment.js-compatible datetime format.
@param {string|Moment} fallback A fallback value to use if the specified date
is null, undefined, or falsy.
*/
formatDate: function(datetime, dtFormat, fallback) {
var momentDate;
if (datetime == null) {
datetime = void 0;
}
if (dtFormat == null) {
dtFormat = 'YYYY-MM';
}
// If a Moment.js object was passed in, just call format on it
if (datetime && moment.isMoment(datetime)) {
return datetime.format(dtFormat);
}
if (String.is(datetime)) {
// If a string was passed in, convert to Moment using the 2-paramter
// constructor with an explicit format string.
momentDate = moment(datetime, dtFormat);
if (momentDate.isValid()) {
return momentDate.format(dtFormat);
}
// If that didn't work, try again with the single-parameter constructor
// but this may throw a deprecation warning
momentDate = moment(datetime);
if (momentDate.isValid()) {
return momentDate.format(dtFormat);
}
}
// We weren't able to format the provided datetime. Now do one of three
// things.
// 1. If datetime is non-null/non-falsy, return it. For this helper,
// string date values that we can't parse are assumed to be display dates.
// 2. If datetime IS null or falsy, use the value from the fallback.
// 3. If the fallback value is specifically 'true', emit 'Present'.
return datetime || (typeof fallback === 'string' ? fallback : (fallback === true ? 'Present' : ''));
},
/**
Emit a formatted string representing the specified datetime.
@param {string} dateValue A raw date value from the FRESH or JRS resume.
@param {string} [dateFormat='YYYY-MM'] The desired datetime format. Must be
compatible with Moment.js datetime formats.
@param {string} [dateDefault=null] The default date value to use if the dateValue
parameter is null, undefined, or falsy.
*/
date: function(dateValue, dateFormat, dateDefault) {
var dateValueMoment, dateValueSafe, reserved;
if (!dateDefault || !String.is(dateDefault)) {
dateDefault = 'Current';
}
if (!dateFormat || !String.is(dateFormat)) {
dateFormat = 'YYYY-MM';
}
if (!dateValue || !String.is(dateValue)) {
dateValue = null;
}
if (!dateValue) {
return dateDefault;
}
reserved = ['current', 'present', 'now'];
dateValueSafe = dateValue.trim().toLowerCase();
if (_.contains(reserved, dateValueSafe)) {
return dateValue;
}
dateValueMoment = moment(dateValue, dateFormat);
if (dateValueMoment.isValid()) {
return dateValueMoment.format(dateFormat);
}
return dateValue;
},
/**
Given a resume sub-object with a start/end date, format a representation of
the date range.
*/
dateRange: function(obj, fmt, sep, fallback) {
if (!obj) {
return '';
}
return _fromTo(obj.start, obj.end, fmt, sep, fallback);
},
/**
Format a from/to date range for display.
@method toFrom
*/
fromTo: function() {
return _fromTo.apply(this, arguments);
},
/**
Return a named color value as an RRGGBB string.
@method toFrom
*/
color: function(colorName, colorDefault) {
var ret;
if (!(colorName && colorName.trim())) {
return _reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontList',
error: HMSTATUS.missingParam,
expected: 'name'
});
} else {
if (!GenericHelpers.theme.colors) {
return colorDefault;
}
ret = GenericHelpers.theme.colors[colorName];
if (!(ret && ret.trim())) {
return colorDefault;
}
return ret;
}
},
/**
Emit the size of the specified named font.
@param key {String} A named style from the "fonts" section of the theme's
theme.json file. For example: 'default' or 'heading1'.
*/
fontSize: function(key, defSize, units) {
var fontSpec, hasDef, ret;
ret = '';
hasDef = defSize && (String.is(defSize) || _.isNumber(defSize));
if (!(key && key.trim())) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontSize',
error: HMSTATUS.missingParam,
expected: 'key'
});
return ret;
} else if (GenericHelpers.theme.fonts) {
fontSpec = LO.get(GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) {
// Check for an "all" format
if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[key];
}
}
if (fontSpec) {
// fontSpec can be a string, an array, or an object
if (String.is(fontSpec)) {
// No font size was specified, only a font family.
} else if (_.isArray(fontSpec)) {
if (!String.is(fontSpec[0])) {
ret = fontSpec[0].size;
}
} else {
// A font description object.
ret = fontSpec.size;
}
}
}
if (!ret) {
if (hasDef) {
ret = defSize;
} else {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontSize',
error: HMSTATUS.missingParam,
expected: 'defSize'
});
ret = '';
}
}
return ret;
},
/**
Emit the font face (such as 'Helvetica' or 'Calibri') associated with the
provided key.
@param key {String} A named style from the "fonts" section of the theme's
theme.json file. For example: 'default' or 'heading1'.
@param defFont {String} The font to use if the specified key isn't present.
Can be any valid font-face name such as 'Helvetica Neue' or 'Calibri'.
*/
fontFace: function(key, defFont) {
var fontSpec, hasDef, ret;
ret = '';
hasDef = defFont && String.is(defFont);
if (!(key && key.trim())) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontFace',
error: HMSTATUS.missingParam,
expected: 'key'
});
return ret;
// If the theme has a "fonts" section, lookup the font face.
} else if (GenericHelpers.theme.fonts) {
fontSpec = LO.get(GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) {
// Check for an "all" format
if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[key];
}
}
if (fontSpec) {
// fontSpec can be a string, an array, or an object
if (String.is(fontSpec)) {
ret = fontSpec;
} else if (_.isArray(fontSpec)) {
// An array of fonts were specified. Each one could be a string
// or an object
ret = String.is(fontSpec[0]) ? fontSpec[0] : fontSpec[0].name;
} else {
// A font description object.
ret = fontSpec.name;
}
}
}
if (!(ret && ret.trim())) {
ret = defFont;
if (!hasDef) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontFace',
error: HMSTATUS.missingParam,
expected: 'defFont'
});
ret = '';
}
}
return ret;
},
fontList: function(key, defFontList, sep) {
var fontSpec, hasDef, ret;
ret = '';
hasDef = defFontList && String.is(defFontList);
if (!(key && key.trim())) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontList',
error: HMSTATUS.missingParam,
expected: 'key'
});
} else if (GenericHelpers.theme.fonts) {
fontSpec = LO.get(GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) {
if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[key];
}
}
if (fontSpec) {
// fontSpec can be a string, an array, or an object
if (String.is(fontSpec)) {
ret = fontSpec;
} else if (_.isArray(fontSpec)) {
// An array of fonts were specified. Each one could be a string
// or an object
fontSpec = fontSpec.map(function(ff) {
return "'" + (String.is(ff) ? ff : ff.name) + "'";
});
ret = fontSpec.join(sep === void 0 ? ', ' : sep || '');
} else {
// A font description object.
ret = fontSpec.name;
}
}
}
if (!(ret && ret.trim())) {
if (!hasDef) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontList',
error: HMSTATUS.missingParam,
expected: 'defFontList'
});
ret = '';
} else {
ret = defFontList;
}
}
return ret;
},
camelCase: function(val) {
val = (val && val.trim()) || '';
if (val) {
return val.charAt(0).toUpperCase() + val.slice(1);
} else {
return val;
}
},
/**
Display a user-overridable section title for a FRESH resume theme. Use this in
lieue of hard-coding section titles.
Usage:
{{sectionTitle "sectionName"}}
{{sectionTitle "sectionName" "sectionTitle"}}
Example:
{{sectionTitle "Education"}}
{{sectionTitle "Employment" "Project History"}}
@param sect_name The name of the section being title. Must be one of the
top-level FRESH resume sections ("info", "education", "employment", etc.).
@param sect_title The theme-specified section title. May be replaced by the
user.
@method sectionTitle
*/
sectionTitle: function(sname, stitle) {
// If not provided by the user, stitle should default to sname. ps.
// Handlebars silently passes in the options object to the last param,
// where in Underscore stitle will be null/undefined, so we check both.
// TODO: not actually sure that's true, given that we _.wrap these functions
stitle = (stitle && String.is(stitle) && stitle) || sname;
// If there's a section title override, use it.
return (this.opts.stitles && this.opts.stitles[sname.toLowerCase().trim()]) || stitle;
},
wpml: function(txt, inline) {
if (!txt) {
return '';
}
inline = (inline && !inline.hash) || false;
txt = XML(txt.trim());
txt = inline ? MD(txt).replace(/^\s*<p>|<\/p>\s*$/gi, '') : MD(txt);
txt = H2W(txt);
return txt;
},
/**
Emit a conditional link.
@method link
*/
link: function(text, url) {
if (url && url.trim()) {
return '<a href="' + url + '">' + text + '</a>';
} else {
return text;
}
},
/**
Emit a conditional Markdown link.
@method link
*/
linkMD: function(text, url) {
if (url && url.trim()) {
return '[' + text + '](' + url + ')';
} else {
return text;
}
},
/**
Return the last word of the specified text.
@method lastWord
*/
lastWord: function(txt) {
if (txt && txt.trim()) {
return _.last(txt.split(' '));
} else {
return '';
}
},
/**
Convert a skill level to an RGB color triplet. TODO: refactor
@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, skillColors;
idx = _skillLevelToIndex(lvl);
skillColors = (this.theme && this.theme.palette && this.theme.palette.skillLevels) || ['#FFFFFF', '#5CB85C', '#F1C40F', '#428BCA', '#C00000'];
return skillColors[idx];
},
/**
Return an appropriate height. TODO: refactor
@method lastWord
*/
skillHeight: function(lvl) {
var idx;
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) {
if (txt && txt.trim()) {
return _.initial(txt.split(' ')).join(' ');
} else {
return '';
}
},
/**
Trim the protocol (http or https) from a URL/
@method trimURL
*/
trimURL: function(url) {
if (url && url.trim()) {
return url.trim().replace(/^https?:\/\//i, '');
} else {
return '';
}
},
/**
Convert text to lowercase.
@method toLower
*/
toLower: function(txt) {
if (txt && txt.trim()) {
return txt.toLowerCase();
} else {
return '';
}
},
/**
Convert text to lowercase.
@method toLower
*/
toUpper: function(txt) {
if (txt && txt.trim()) {
return txt.toUpperCase();
} else {
return '';
}
},
/**
Conditional stylesheet link. Creates a link to the specified stylesheet with
<link> or embeds the styles inline with <style></style>, depending on the
theme author's and user's preferences.
@param url {String} The path to the CSS file.
@param linkage {String} The default link method. Can be either `embed` or
`link`. If omitted, defaults to `embed`. Can be overridden by the `--css`
command-line switch.
*/
styleSheet: function(url, linkage) {
var rawCss, renderedCss, ret;
// Establish the linkage style
linkage = this.opts.css || linkage || 'embed';
ret = '';
if (linkage === 'link') {
ret = printf('<link href="%s" rel="stylesheet" type="text/css">', url);
} else {
rawCss = FS.readFileSync(PATH.join(this.opts.themeObj.folder, '/src/', url), 'utf8');
renderedCss = this.engine.generateSimple(this, rawCss);
ret = printf('<style>%s</style>', renderedCss);
}
// If the currently-executing template is inherited, append styles
if (this.opts.themeObj.inherits && this.opts.themeObj.inherits.html && this.format === 'html') {
ret += linkage === 'link' ? '<link href="' + this.opts.themeObj.overrides.path + '" rel="stylesheet" type="text/css">' : '<style>' + this.opts.themeObj.overrides.data + '</style>';
}
// TODO: It would be nice to use Handlebar.SafeString here, but these
// are supposed to be generic helpers. Provide an equivalent, or expose
// it when Handlebars is the chosen engine, which is most of the time.
return ret;
},
/**
Perform a generic comparison.
See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates
@method compare
*/
compare: function(lvalue, rvalue, options) {
var operator, operators, result;
if (arguments.length < 3) {
throw new Error("Template helper 'compare' needs 2 parameters");
}
operator = options.hash.operator || "==";
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("Helper 'compare' doesn't know the operator " + operator);
}
result = operators[operator](lvalue, rvalue);
if (result) {
return options.fn(this);
} else {
return options.inverse(this);
}
},
/**
Emit padded text.
*/
pad: function(stringOrArray, padAmount, unused) {
var PAD, ret;
stringOrArray = stringOrArray || '';
padAmount = padAmount || 0;
ret = '';
PAD = require('string-padding');
if (!String.is(stringOrArray)) {
ret = stringOrArray.map(function(line) {
return PAD(line, line.length + Math.abs(padAmount), null, padAmount < 0 ? PAD.LEFT : PAD.RIGHT);
}).join('\n');
} else {
ret = PAD(stringOrArray, stringOrArray.length + Math.abs(padAmount), null, padAmount < 0 ? PAD.LEFT : PAD.RIGHT);
}
return ret;
},
/**
Given the name of a skill ("JavaScript" or "HVAC repair"), return the number
of years assigned to that skill in the resume.skills.list collection.
*/
skillYears: function(skill, rez) {
var sk;
sk = _.find(rez.skills.list, function(sk) {
return sk.name.toUpperCase() === skill.toUpperCase();
});
if (sk) {
return sk.years;
} else {
return '?';
}
},
/**
Given an object that may be a string or an object, return it as-is if it's a
string, otherwise return the value at obj[objPath].
*/
stringOrObject: function(obj, objPath, rez) {
if (_.isString(obj)) {
return obj;
} else {
return LO.get(obj, objPath);
}
}
};
_reportError = function(code, params) {
return GenericHelpers.opts.errHandler.err(code, params);
};
_fromTo = function(dateA, dateB, fmt, sep, fallback) {
var dateATrim, dateBTrim, dateFrom, dateTemp, dateTo, reserved;
// Prevent accidental use of safe.start, safe.end, safe.date
// The dateRange helper is for raw dates only
if (moment.isMoment(dateA) || moment.isMoment(dateB)) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'dateRange'
});
return '';
}
dateFrom = null;
dateTo = null;
dateTemp = null;
// Check for 'current', 'present', 'now', '', null, and undefined
dateA = dateA || '';
dateB = dateB || '';
dateATrim = dateA.trim().toLowerCase();
dateBTrim = dateB.trim().toLowerCase();
reserved = ['current', 'present', 'now', ''];
fmt = (fmt && String.is(fmt) && fmt) || 'YYYY-MM';
sep = (sep && String.is(sep) && sep) || ' — ';
if (_.contains(reserved, dateATrim)) {
dateFrom = fallback || '???';
} else {
dateTemp = FluentDate.fmt(dateA);
dateFrom = dateTemp.format(fmt);
}
if (_.contains(reserved, dateBTrim)) {
dateTo = fallback || 'Present';
} else {
dateTemp = FluentDate.fmt(dateB);
dateTo = dateTemp.format(fmt);
}
if (dateFrom === dateTo) {
return dateFrom;
} else if (dateFrom && dateTo) {
return dateFrom + sep + dateTo;
} else if (dateFrom || dateTo) {
return dateFrom || dateTo;
}
return '';
};
_skillLevelToIndex = function(lvl) {
var idx, intVal;
idx = 0;
if (String.is(lvl)) {
lvl = lvl.trim().toLowerCase();
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;
}
} 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;
};
// Note [1] --------------------------------------------------------------------
// Make sure it's precisely a string or array since some template engines jam
// their options/context object into the last parameter and we are allowing the
// defFont parameter to be omitted in certain cases. This is a little kludgy,
// but works fine for this case. If we start doing this regularly, we should
// rebind these parameters.
// Note [2]: -------------------------------------------------------------------
// If execution reaches here, some sort of cosmic ray or sunspot has landed on
// HackMyResume, or a theme author is deliberately messing with us by doing
// something like:
// "fonts": {
// "default": "",
// "heading1": null
// }
// Rather than sort it out, we'll just fall back to defFont.
}).call(this);
//# sourceMappingURL=generic-helpers.js.map

View File

@ -1,97 +0,0 @@
(function() {
/**
Template helper definitions for Handlebars.
@license MIT. See LICENSE.md for details.
@module handlebars-helpers.js
*/
var HANDLEBARS, HMS, _, blockHelpers, helpers, path;
HANDLEBARS = require('handlebars');
_ = require('underscore');
helpers = require('./generic-helpers');
path = require('path');
blockHelpers = require('./block-helpers');
HMS = require('../core/status-codes');
/**
Register useful Handlebars helpers.
@method registerHelpers
*/
module.exports = function(theme, rez, opts) {
var curGlob, ex, glob, slash, wrappedHelpers;
helpers.theme = theme;
helpers.opts = opts;
helpers.type = 'handlebars';
// Prepare generic helpers for use with Handlebars. We do this by wrapping them
// in a Handlebars-aware wrapper which calls the helper internally.
wrappedHelpers = _.mapObject(helpers, function(hVal, hKey) {
if (_.isFunction(hVal)) {
return _.wrap(hVal, function(func) {
var args;
args = Array.prototype.slice.call(arguments);
args.shift(); // lose the 1st element (func) [^1]
//args.pop() # lose the last element (HB options hash)
args[args.length - 1] = rez; // replace w/ resume object
return func.apply(this, args); // call the generic helper
});
}
return hVal;
}, this);
HANDLEBARS.registerHelper(wrappedHelpers);
// Prepare Handlebars-specific helpers - "blockHelpers" is really a misnomer
// since any kind of Handlebars-specific helper can live here
HANDLEBARS.registerHelper(blockHelpers);
if (_.isString(theme.helpers)) {
// Register any theme-provided custom helpers...
// Normalize "theme.helpers" (string or array) to an array
theme.helpers = [theme.helpers];
}
if (_.isArray(theme.helpers)) {
glob = require('glob');
slash = require('slash');
curGlob = null;
try {
_.each(theme.helpers, function(fGlob) { // foreach theme.helpers entry
var files;
curGlob = fGlob; // ..cache in case of exception
fGlob = path.join(theme.folder, fGlob); // ..make relative to theme
files = glob.sync(slash(fGlob)); // ..expand the glob
if (files.length > 0) { // ..guard against empty glob
_.each(files, function(f) { // ..loop over concrete paths
HANDLEBARS.registerHelper(require(f)); // ..register the path
});
} else {
throw {
fluenterror: HMS.themeHelperLoad,
inner: er,
glob: fGlob
};
}
});
} catch (error) {
ex = error;
throw {
fluenterror: HMS.themeHelperLoad,
inner: ex,
glob: curGlob,
exit: true
};
}
}
};
// [^1]: This little bit of acrobatics ensures that our generic helpers are
// called as generic helpers, not as Handlebars-specific helpers. This allows
// them to be used in other templating engines, like Underscore. If you need a
// Handlebars-specific helper with normal Handlebars context and options, put it
// in block-helpers.coffee.
}).call(this);
//# sourceMappingURL=handlebars-helpers.js.map

View File

@ -1,34 +0,0 @@
(function() {
/**
Template helper definitions for Underscore.
@license MIT. See LICENSE.md for details.
@module handlebars-helpers.js
*/
var HANDLEBARS, _, helpers;
HANDLEBARS = require('handlebars');
_ = require('underscore');
helpers = require('./generic-helpers');
/**
Register useful Underscore helpers.
@method registerHelpers
*/
module.exports = function(theme, opts, cssInfo, ctx, eng) {
helpers.theme = theme;
helpers.opts = opts;
helpers.cssInfo = cssInfo;
helpers.engine = eng;
ctx.h = helpers;
_.each(helpers, function(hVal, hKey) {
if (_.isFunction(hVal)) {
return _.bind(hVal, ctx);
}
}, this);
};
}).call(this);
//# sourceMappingURL=underscore-helpers.js.map

44
dist/index.js vendored
View File

@ -1,44 +0,0 @@
(function() {
/**
External API surface for HackMyResume.
@license MIT. See LICENSE.md for details.
@module hackmycore/index
*/
/** API facade for HackMyResume. */
module.exports = {
verbs: {
build: require('./verbs/build'),
analyze: require('./verbs/analyze'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
new: require('./verbs/create'),
peek: require('./verbs/peek')
},
alias: {
generate: require('./verbs/build'),
create: require('./verbs/create')
},
options: require('./core/default-options'),
formats: require('./core/default-formats'),
Sheet: require('./core/fresh-resume'),
FRESHResume: require('./core/fresh-resume'),
JRSResume: require('./core/jrs-resume'),
FRESHTheme: require('./core/fresh-theme'),
JRSTheme: require('./core/jrs-theme'),
ResumeFactory: require('./core/resume-factory'),
FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./generators/html-generator'),
TextGenerator: require('./generators/text-generator'),
HtmlPdfCliGenerator: require('./generators/html-pdf-cli-generator'),
WordGenerator: require('./generators/word-generator'),
MarkdownGenerator: require('./generators/markdown-generator'),
JsonGenerator: require('./generators/json-generator'),
YamlGenerator: require('./generators/yaml-generator'),
JsonYamlGenerator: require('./generators/json-yaml-generator'),
LaTeXGenerator: require('./generators/latex-generator'),
HtmlPngGenerator: require('./generators/html-png-generator')
};
}).call(this);
//# sourceMappingURL=index.js.map

View File

@ -1,65 +0,0 @@
(function() {
var FluentDate, _, lo;
FluentDate = require('../core/fluent-date');
_ = require('underscore');
lo = require('lodash');
module.exports = {
/**
Compute the total duration of the work history.
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
run: function(rez, collKey, startKey, endKey, unit) {
var firstDate, hist, lastDate, new_e;
unit = unit || 'years';
hist = lo.get(rez, collKey);
if (!hist || !hist.length) {
return 0;
}
// BEGIN CODE DUPLICATION --> src/inspectors/gap-inspector.coffee (TODO)
// Convert the candidate's employment history to an array of dates,
// where each element in the array is a start date or an end date of a
// job -- it doesn't matter which.
new_e = hist.map(function(job) {
var obj;
obj = _.pick(job, [startKey, endKey]);
if (!_.has(obj, endKey)) {
// Synthesize an end date if this is a "current" gig
obj[endKey] = 'current';
}
if (obj && (obj[startKey] || obj[endKey])) {
obj = _.pairs(obj);
obj[0][1] = FluentDate.fmt(obj[0][1]);
if (obj.length > 1) {
obj[1][1] = FluentDate.fmt(obj[1][1]);
}
}
return obj;
});
// Flatten the array, remove empties, and sort
new_e = _.filter(_.flatten(new_e, true), function(v) {
return v && v.length && v[0] && v[0].length;
});
if (!new_e || !new_e.length) {
return 0;
}
new_e = _.sortBy(new_e, function(elem) {
return elem[1].unix();
});
// END CODE DUPLICATION
firstDate = _.first(new_e)[1];
lastDate = _.last(new_e)[1];
return lastDate.diff(firstDate, unit);
}
};
}).call(this);
//# sourceMappingURL=duration-inspector.js.map

View File

@ -1,154 +0,0 @@
(function() {
/**
Employment gap analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module inspectors/gap-inspector
*/
/**
Identify gaps in the candidate's employment history.
*/
var FluentDate, LO, _, gapInspector, moment;
_ = require('underscore');
FluentDate = require('../core/fluent-date');
moment = require('moment');
LO = require('lodash');
gapInspector = module.exports = {
moniker: 'gap-inspector',
/**
Run the Gap Analyzer on a resume.
@method run
@return {Array} An array of object representing gaps in the candidate's
employment history. Each object provides the start, end, and duration of the
gap:
{ <-- gap
start: // A Moment.js date
end: // A Moment.js date
duration: // Gap length
}
*/
run: function(rez) {
var coverage, dur, g, gap_start, hist, new_e, num_gaps, o, ref_count, tdur, total_gap_days;
// This is what we'll return
coverage = {
gaps: [],
overlaps: [],
pct: '0%',
duration: {
total: 0,
work: 0,
gaps: 0
}
};
// Missing employment section? Bye bye.
hist = LO.get(rez, 'employment.history');
if (!hist || !hist.length) {
return coverage;
}
// Convert the candidate's employment history to an array of dates,
// where each element in the array is a start date or an end date of a
// job -- it doesn't matter which.
new_e = hist.map(function(job) {
var obj;
obj = _.pick(job, ['start', 'end']);
if (obj && (obj.start || obj.end)) {
obj = _.pairs(obj);
obj[0][1] = FluentDate.fmt(obj[0][1]);
if (obj.length > 1) {
obj[1][1] = FluentDate.fmt(obj[1][1]);
}
}
return obj;
});
// Flatten the array, remove empties, and sort
new_e = _.filter(_.flatten(new_e, true), function(v) {
return v && v.length && v[0] && v[0].length;
});
if (!new_e || !new_e.length) {
return coverage;
}
new_e = _.sortBy(new_e, function(elem) {
return elem[1].unix();
});
// Iterate over elements in the array. Each time a start date is found,
// increment a reference count. Each time an end date is found, decrement
// the reference count. When the reference count reaches 0, we have a gap.
// When the reference count is > 0, the candidate is employed. When the
// reference count reaches 2, the candidate is overlapped.
num_gaps = 0;
ref_count = 0;
total_gap_days = 0;
gap_start = null;
new_e.forEach(function(point) {
var inc, lastGap, lastOver;
inc = point[0] === 'start' ? 1 : -1;
ref_count += inc;
// If the ref count just reached 0, start a new GAP
if (ref_count === 0) {
return coverage.gaps.push({
start: point[1],
end: null
});
// If the ref count reached 1 by rising, end the last GAP
} else if (ref_count === 1 && inc === 1) {
lastGap = _.last(coverage.gaps);
if (lastGap) {
lastGap.end = point[1];
lastGap.duration = lastGap.end.diff(lastGap.start, 'days');
return total_gap_days += lastGap.duration;
}
// If the ref count reaches 2 by rising, start a new OVERLAP
} else if (ref_count === 2 && inc === 1) {
return coverage.overlaps.push({
start: point[1],
end: null
});
// If the ref count reaches 1 by falling, end the last OVERLAP
} else if (ref_count === 1 && inc === -1) {
lastOver = _.last(coverage.overlaps);
if (lastOver) {
lastOver.end = point[1];
lastOver.duration = lastOver.end.diff(lastOver.start, 'days');
if (lastOver.duration === 0) {
return coverage.overlaps.pop();
}
}
}
});
// It's possible that the last gap/overlap didn't have an explicit .end
// date.If so, set the end date to the present date and compute the
// duration normally.
if (coverage.overlaps.length) {
o = _.last(coverage.overlaps);
if (o && !o.end) {
o.end = moment();
o.duration = o.end.diff(o.start, 'days');
}
}
if (coverage.gaps.length) {
g = _.last(coverage.gaps);
if (g && !g.end) {
g.end = moment();
g.duration = g.end.diff(g.start, 'days');
}
}
// Package data for return to the client
tdur = rez.duration('days');
dur = {
total: tdur,
work: tdur - total_gap_days,
gaps: total_gap_days
};
coverage.pct = dur.total > 0 && dur.work > 0 ? (((dur.total - dur.gaps) / dur.total) * 100).toFixed(1) + '%' : '???';
coverage.duration = dur;
return coverage;
}
};
}).call(this);
//# sourceMappingURL=gap-inspector.js.map

View File

@ -1,78 +0,0 @@
(function() {
/**
Keyword analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module inspectors/keyword-inspector
*/
/**
Analyze the resume's use of keywords.
TODO: BUG: Keyword search regex is inaccurate, especially for one or two
letter keywords like "C" or "CLI".
@class keywordInspector
*/
var FluentDate, _, keywordInspector;
_ = require('underscore');
FluentDate = require('../core/fluent-date');
keywordInspector = module.exports = {
/** A unique name for this inspector. */
moniker: 'keyword-inspector',
/**
Run the Keyword Inspector on a resume.
@method run
@return An collection of statistical keyword data.
*/
run: function(rez) {
var prefix, regex_quote, searchable, suffix;
// "Quote" or safely escape a keyword so it can be used as a regex. For
// example, if the keyword is "C++", yield "C\+\+".
// http://stackoverflow.com/a/2593661/4942583
regex_quote = function(str) {
return (str + '').replace(/[.?*+^$[\]\\(){}|-]/ig, "\\$&");
};
// Create a searchable plain-text digest of the resume
// TODO: BUG: Don't search within keywords for other keywords. Job A
// declares the "foo" keyword. Job B declares the "foo & bar" keyword. Job
// B's mention of "foobar" should not count as a mention of "foo".
// To achieve this, remove keywords from the search digest and treat them
// separately.
searchable = '';
rez.transformStrings(['imp', 'computed', 'safe'], function(key, val) {
return searchable += ' ' + val;
});
// Assemble a regex skeleton we can use to test for keywords with a bit
// more
prefix = '(?:' + ['^', '\\s+', '[\\.,]+'].join('|') + ')';
suffix = '(?:' + ['$', '\\s+', '[\\.,]+'].join('|') + ')';
return rez.keywords().map(function(kw) {
var count, myArray, regex, regex_str;
// 1. Using word boundary or other regex class is inaccurate
// var regex = new RegExp( '\\b' + regex_quote( kw )/* + '\\b'*/, 'ig');
// 2. Searching for the raw keyword is inaccurate ("C" will match any
// word containing a 'c'!).
// var regex = new RegExp( regex_quote( kw ), 'ig');
// 3. Instead, use a custom regex with special delimeters.
regex_str = prefix + regex_quote(kw) + suffix;
regex = new RegExp(regex_str, 'ig');
myArray = null;
count = 0;
while ((myArray = regex.exec(searchable)) !== null) {
count++;
}
return {
name: kw,
count: count
};
});
}
};
}).call(this);
//# sourceMappingURL=keyword-inspector.js.map

View File

@ -1,46 +0,0 @@
(function() {
/**
Section analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module inspectors/totals-inspector
*/
/**
Retrieve sectional overview and summary information.
@class totalsInspector
*/
var FluentDate, _, totalsInspector;
_ = require('underscore');
FluentDate = require('../core/fluent-date');
totalsInspector = module.exports = {
moniker: 'totals-inspector',
/**
Run the Totals Inspector on a resume.
@method run
@return An object containing summary information for each section on the
resume.
*/
run: function(rez) {
var sectionTotals;
sectionTotals = {};
_.each(rez, function(val, key) {
if (_.isArray(val) && !_.isString(val)) {
return sectionTotals[key] = val.length;
} else if (val.history && _.isArray(val.history)) {
return sectionTotals[key] = val.history.length;
} else if (val.sets && _.isArray(val.sets)) {
return sectionTotals[key] = val.sets.length;
}
});
return {
totals: sectionTotals,
numSections: Object.keys(sectionTotals).length
};
}
};
}).call(this);
//# sourceMappingURL=totals-inspector.js.map

View File

@ -1,109 +0,0 @@
(function() {
/**
Definition of the HandlebarsGenerator class.
@license MIT. See LICENSE.md for details.
@module renderers/handlebars-generator
*/
/**
Perform template-based resume generation using Handlebars.js.
@class HandlebarsGenerator
*/
var FS, HANDLEBARS, HMSTATUS, HandlebarsGenerator, PATH, READFILES, SLASH, _, parsePath, registerHelpers, registerPartials;
_ = require('underscore');
HANDLEBARS = require('handlebars');
FS = require('fs');
registerHelpers = require('../helpers/handlebars-helpers');
PATH = require('path');
parsePath = require('parse-filepath');
READFILES = require('recursive-readdir-sync');
HMSTATUS = require('../core/status-codes');
SLASH = require('slash');
HandlebarsGenerator = module.exports = {
generateSimple: function(data, tpl) {
var err, template;
try {
// Compile and run the Handlebars template.
template = HANDLEBARS.compile(tpl, {
strict: false,
assumeObjects: false,
noEscape: data.opts.noescape
});
return template(data);
} catch (error1) {
err = error1;
throw {
fluenterror: HMSTATUS[template ? 'invokeTemplate' : 'compileTemplate'],
inner: err
};
}
},
generate: function(json, jst, format, curFmt, opts, theme) {
var ctx, encData;
// Preprocess text
encData = json;
if (format === 'html' || format === 'pdf') {
encData = json.markdownify();
}
if (format === 'doc') {
encData = json.xmlify();
}
// Set up partials and helpers
registerPartials(format, theme);
registerHelpers(theme, encData, opts);
// Set up the context
ctx = {
r: encData,
RAW: json,
filt: opts.filters,
format: format,
opts: opts,
engine: this,
results: curFmt.files,
headFragment: opts.headFragment || ''
};
// Render the template
return this.generateSimple(ctx, jst);
}
};
registerPartials = function(format, theme) {
var partialsFolder;
if (_.contains(['html', 'doc', 'md', 'txt', 'pdf'], format)) {
// Locate the global partials folder
partialsFolder = PATH.join(parsePath(require.resolve('fresh-themes')).dirname, '/partials/', format === 'pdf' ? 'html' : format);
// Register global partials in the /partials/[format] folder
// TODO: Only do this once per HMR invocation.
_.each(READFILES(partialsFolder, function(error) {
return {};
}), function(el) {
var compiledTemplate, name, pathInfo, tplData;
pathInfo = parsePath(el);
name = SLASH(PATH.relative(partialsFolder, el).replace(/\.(?:html|xml|hbs|md|txt)$/i, ''));
tplData = FS.readFileSync(el, 'utf8');
compiledTemplate = HANDLEBARS.compile(tplData);
HANDLEBARS.registerPartial(name, compiledTemplate);
return theme.partialsInitialized = true;
});
}
// Register theme-specific partials
return _.each(theme.partials, function(el) {
var compiledTemplate, tplData;
tplData = FS.readFileSync(el.path, 'utf8');
compiledTemplate = HANDLEBARS.compile(tplData);
return HANDLEBARS.registerPartial(el.name, compiledTemplate);
});
};
}).call(this);
//# sourceMappingURL=handlebars-generator.js.map

View File

@ -1,61 +0,0 @@
(function() {
/**
Definition of the JRSGenerator class.
@license MIT. See LICENSE.md for details.
@module renderers/jrs-generator
*/
/**
Perform template-based resume generation for JSON Resume themes.
@class JRSGenerator
*/
var FS, HANDLEBARS, JRSGenerator, MD, MDIN, PATH, READFILES, SLASH, _, parsePath, registerHelpers;
_ = require('underscore');
HANDLEBARS = require('handlebars');
FS = require('fs');
registerHelpers = require('../helpers/handlebars-helpers');
PATH = require('path');
parsePath = require('parse-filepath');
READFILES = require('recursive-readdir-sync');
SLASH = require('slash');
MD = require('marked');
JRSGenerator = module.exports = {
generate: function(json, jst, format, cssInfo, opts, theme) {
var org, rezHtml, turnoff;
// Disable JRS theme chatter (console.log, console.error, etc.)
turnoff = ['log', 'error', 'dir'];
org = turnoff.map(function(c) {
var ret;
ret = console[c];
console[c] = function() {};
return ret;
});
// Freeze and render
rezHtml = theme.render(json.harden());
// Turn logging back on
turnoff.forEach(function(c, idx) {
return console[c] = org[idx];
});
// Unfreeze and apply Markdown
return rezHtml = rezHtml.replace(/@@@@~[\s\S]*?~@@@@/g, function(val) {
return MDIN(val.replace(/~@@@@/g, '').replace(/@@@@~/g, ''));
});
}
};
MDIN = function(txt) { // TODO: Move this
return MD(txt || '').replace(/^\s*<p>|<\/p>\s*$/gi, '');
};
}).call(this);
//# sourceMappingURL=jrs-generator.js.map

View File

@ -1,91 +0,0 @@
(function() {
/**
Definition of the UnderscoreGenerator class.
@license MIT. See LICENSE.md for details.
@module underscore-generator.js
*/
/**
Perform template-based resume generation using Underscore.js.
@class UnderscoreGenerator
*/
var UnderscoreGenerator, _, escapeLaTeX, registerHelpers;
_ = require('underscore');
registerHelpers = require('../helpers/underscore-helpers');
require('../utils/string');
escapeLaTeX = require('escape-latex');
UnderscoreGenerator = module.exports = {
generateSimple: function(data, tpl) {
var HMS, err, t;
try {
// Compile and run the Handlebars template.
t = _.template(tpl);
return t(data);
} catch (error) {
err = error;
//console.dir _error
HMS = require('../core/status-codes');
throw {
fluenterror: HMS[t ? 'invokeTemplate' : 'compileTemplate'],
inner: err
};
}
},
generate: function(json, jst, format, cssInfo, opts, theme) {
var ctx, delims, r, traverse;
// Tweak underscore's default template delimeters
delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template;
if (opts.themeObj && opts.themeObj.delimeters) {
delims = _.mapObject(delims, function(val, key) {
return new RegExp(val, "ig");
});
}
_.templateSettings = delims;
r = null;
switch (format) {
case 'html':
r = json.markdownify();
break;
case 'pdf':
r = json.markdownify();
break;
case 'png':
r = json.markdownify();
break;
case 'latex':
traverse = require('traverse');
r = traverse(json).map(function(x) {
if (this.isLeaf && String.is(this.node)) {
return escapeLaTeX(this.node);
}
return this.node;
});
break;
default:
r = json;
}
// Set up the context
ctx = {
r: r,
filt: opts.filters,
XML: require('xml-escape'),
RAW: json,
cssInfo: cssInfo,
//engine: @
headFragment: opts.headFragment || '',
opts: opts
};
// Link to our helpers
registerHelpers(theme, opts, cssInfo, ctx, this);
// Generate!
return this.generateSimple(ctx, jst);
}
};
}).call(this);
//# sourceMappingURL=underscore-generator.js.map

View File

@ -1,12 +0,0 @@
(function() {
/**
Definition of the SyntaxErrorEx class.
@module file-contains.js
*/
module.exports = function(file, needle) {
return require('fs').readFileSync(file, 'utf-8').indexOf(needle) > -1;
};
}).call(this);
//# sourceMappingURL=file-contains.js.map

View File

@ -1,27 +0,0 @@
(function() {
/**
Defines a regex suitable for matching FRESH versions.
@module file-contains.js
*/
// Set up a regex that matches all of the following:
// - FRESH
// - JRS
// - FRESCA
// - FRESH@1.0.0
// - FRESH@1.0
// - FRESH@1
// - JRS@0.16.0
// - JRS@0.16
// - JRS@0
// Don't use a SEMVER regex (eg, NPM's semver-regex) because a) we want to
// support partial semvers like "0" or "1.2" and b) we'll expand this later to
// support fully scoped FRESH versions.
module.exports = function() {
return RegExp('^(FRESH|FRESCA|JRS)(?:@(\\d+(?:\\.\\d+)?(?:\\.\\d+)?))?$');
};
}).call(this);
//# sourceMappingURL=fresh-version-regex.js.map

View File

@ -1,67 +0,0 @@
(function() {
/**
Definition of the Markdown to WordProcessingML conversion routine.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module utils/html-to-wpml
*/
var HTML5Tokenizer, XML, _;
XML = require('xml-escape');
_ = require('underscore');
HTML5Tokenizer = require('simple-html-tokenizer');
module.exports = function(html) {
var final, is_bold, is_italic, is_link, link_url, tokens;
// Tokenize the HTML stream.
tokens = HTML5Tokenizer.tokenize(html);
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) {
var style;
switch (tok.type) {
case 'StartTag':
switch (tok.tagName) {
case 'p':
return final += '<w:p>';
case 'strong':
return is_bold = true;
case 'em':
return is_italic = true;
case 'a':
is_link = true;
return link_url = tok.attributes.filter(function(attr) {
return attr[0] === 'href';
})[0][1];
}
break;
case 'EndTag':
switch (tok.tagName) {
case 'p':
return final += '</w:p>';
case 'strong':
return is_bold = false;
case 'em':
return is_italic = false;
case 'a':
return is_link = false;
}
break;
case 'Chars':
if ((tok.chars.trim().length)) {
style = is_bold ? '<w:b/>' : '';
style += is_italic ? '<w:i/>' : '';
style += is_link ? '<w:rStyle w:val="Hyperlink"/>' : '';
return final += (is_link ? '<w:hlink w:dest="' + link_url + '">' : '') + '<w:r><w:rPr>' + style + '</w:rPr><w:t>' + XML(tok.chars) + '</w:t></w:r>' + (is_link ? '</w:hlink>' : '');
}
}
});
return final;
};
}).call(this);
//# sourceMappingURL=html-to-wpml.js.map

View File

@ -1,28 +0,0 @@
(function() {
/**
Inline Markdown-to-Chalk conversion routines.
@license MIT. See LICENSE.md for details.
@module utils/md2chalk
*/
var CHALK, LO, MD;
MD = require('marked');
CHALK = require('chalk');
LO = require('lodash');
module.exports = function(v, style, boldStyle) {
var temp;
boldStyle = boldStyle || 'bold';
temp = v.replace(/\*\*(.*?)\*\*/g, LO.get(CHALK, boldStyle)('$1'));
if (style) {
return LO.get(CHALK, style)(temp);
} else {
return temp;
}
};
}).call(this);
//# sourceMappingURL=md2chalk.js.map

View File

@ -1,19 +0,0 @@
(function() {
/**
Definition of the ResumeDetector class.
@module utils/resume-detector
@license MIT. See LICENSE.md for details.
*/
module.exports = function(rez) {
if (rez.meta && rez.meta.format) { //&& rez.meta.format.substr(0, 5).toUpperCase() == 'FRESH'
return 'fresh';
} else if (rez.basics) {
return 'jrs';
} else {
return 'unk';
}
};
}).call(this);
//# sourceMappingURL=resume-detector.js.map

View File

@ -1,55 +0,0 @@
(function() {
module.exports = {
/**
Removes ignored or private fields from a resume object
@returns an object with the following structure:
{
scrubbed: the processed resume object
ignoreList: an array of ignored nodes that were removed
privateList: an array of private nodes that were removed
}
*/
scrubResume: function(rep, opts) {
var ignoreList, includePrivates, privateList, scrubbed, traverse;
traverse = require('traverse');
ignoreList = [];
privateList = [];
includePrivates = opts && opts.private;
scrubbed = traverse(rep).map(function() { // [^1]
if (!this.isLeaf) {
if (this.node.ignore === true || this.node.ignore === 'true') {
ignoreList.push(this.node);
this.delete();
} else if ((this.node.private === true || this.node.private === 'true') && !includePrivates) {
privateList.push(this.node);
this.delete();
}
}
if (_.isArray(this.node)) { // [^2]
this.after(function() {
this.update(_.compact(this.node));
});
}
});
return {
scrubbed: scrubbed,
ingoreList: ignoreList,
privateList: privateList
};
}
};
// [^1]: As of v0.6.6, the NPM traverse library has a quirk when attempting
// to remove array elements directly using traverse's `this.remove`. See:
// https://github.com/substack/js-traverse/issues/48
// [^2]: The workaround is to use traverse's 'this.delete' to nullify the value
// first, followed by removal with something like _.compact.
// https://github.com/substack/js-traverse/issues/48#issuecomment-142607200
}).call(this);
//# sourceMappingURL=resume-scrubber.js.map

View File

@ -1,35 +0,0 @@
(function() {
/**
Definition of the SafeJsonLoader class.
@module utils/safe-json-loader
@license MIT. See LICENSE.md for details.
*/
var FS, SyntaxErrorEx;
FS = require('fs');
SyntaxErrorEx = require('./syntax-error-ex');
module.exports = function(file) {
var err, ret, retRaw;
ret = {};
try {
ret.raw = FS.readFileSync(file, 'utf8');
ret.json = JSON.parse(ret.raw);
} catch (error) {
err = error;
// If we get here, either FS.readFileSync or JSON.parse failed.
// We'll return HMSTATUS.readError or HMSTATUS.parseError.
retRaw = ret.raw && ret.raw.trim();
ret.ex = {
op: retRaw ? 'parse' : 'read',
inner: SyntaxErrorEx.is(err) ? new SyntaxErrorEx(err, retRaw) : err,
file: file
};
}
return ret;
};
}).call(this);
//# sourceMappingURL=safe-json-loader.js.map

View File

@ -1,43 +0,0 @@
(function() {
/**
Safe spawn utility for HackMyResume / FluentCV.
@module utils/safe-spawn
@license MIT. See LICENSE.md for details.
*/
/** Safely spawn a process synchronously or asynchronously without throwing an
exception */
module.exports = function(cmd, args, isSync, callback, param) {
var ex, info, spawn;
try {
// .spawnSync not available on earlier Node.js, so default to .spawn
spawn = require('child_process')[isSync ? 'spawnSync' : 'spawn'];
info = spawn(cmd, args);
if (!isSync) {
info.on('error', function(err) {
if (typeof callback === "function") {
callback(err, param);
}
});
} else {
if (info.error) {
if (typeof callback === "function") {
callback(info.error, param);
}
return {
cmd: cmd,
inner: info.error
};
}
}
} catch (error) {
ex = error;
if (typeof callback === "function") {
callback(ex, param);
}
return ex;
}
};
}).call(this);
//# sourceMappingURL=safe-spawn.js.map

View File

@ -1,61 +0,0 @@
(function() {
/**
Object string transformation.
@module utils/string-transformer
@license MIT. See LICENSE.md for details.
*/
var _, moment;
_ = require('underscore');
moment = require('moment');
/**
Create a copy of this object in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder).
*/
module.exports = function(ret, filt, transformer) {
var that, transformStringsInObject;
that = this;
// TODO: refactor recursion
transformStringsInObject = function(obj, filters) {
if (!obj) {
return;
}
if (moment.isMoment(obj)) {
return;
}
if (_.isArray(obj)) {
return obj.forEach(function(elem, idx, ar) {
if (typeof elem === 'string' || elem instanceof String) {
return ar[idx] = transformer(null, elem);
} else if (_.isObject(elem)) {
return transformStringsInObject(elem, filters);
}
});
} else if (_.isObject(obj)) {
return Object.keys(obj).forEach(function(k) {
var sub;
if (filters.length && _.contains(filters, k)) {
return;
}
sub = obj[k];
if (typeof sub === 'string' || sub instanceof String) {
return obj[k] = transformer(k, sub);
} else if (_.isObject(sub)) {
return transformStringsInObject(sub, filters);
}
});
}
};
Object.keys(ret).forEach(function(member) {
if (!filt || !filt.length || !_.contains(filt, member)) {
return transformStringsInObject(ret[member], filt || []);
}
});
return ret;
};
}).call(this);
//# sourceMappingURL=string-transformer.js.map

25
dist/utils/string.js vendored
View File

@ -1,25 +0,0 @@
(function() {
/**
Definitions of string utility functions.
@module utils/string
*/
/**
Determine if the string is null, empty, or whitespace.
See: http://stackoverflow.com/a/32800728/4942583
@method isNullOrWhitespace
*/
String.isNullOrWhitespace = function(input) {
return !input || !input.trim();
};
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;
};
}).call(this);
//# sourceMappingURL=string.js.map

View File

@ -1,51 +0,0 @@
(function() {
/**
Definition of the SyntaxErrorEx class.
@module utils/syntax-error-ex
@license MIT. See LICENSE.md for details.
*/
var SyntaxErrorEx;
/**
Represents a SyntaxError exception with line and column info.
Collect syntax error information from the provided exception object. The
JavaScript `SyntaxError` exception isn't interpreted uniformly across environ-
ments, so we reparse on error to grab the line and column.
See: http://stackoverflow.com/q/13323356
@class SyntaxErrorEx
*/
SyntaxErrorEx = class SyntaxErrorEx {
constructor(ex, rawData) {
var JSONLint, colNum, err, lineNum, lint;
lineNum = null;
colNum = null;
JSONLint = require('json-lint');
lint = JSONLint(rawData, {
comments: false
});
if (lint.error) {
[this.line, this.col] = [lint.line, lint.character];
}
if (!lint.error) {
JSONLint = require('jsonlint');
try {
JSONLint.parse(rawData);
} catch (error) {
err = error;
this.line = (/on line (\d+)/.exec(err))[1];
}
}
}
};
// Return true if the supplied parameter is a JavaScript SyntaxError
SyntaxErrorEx.is = function(ex) {
return ex instanceof SyntaxError;
};
module.exports = SyntaxErrorEx;
}).call(this);
//# sourceMappingURL=syntax-error-ex.js.map

99
dist/verbs/analyze.js vendored
View File

@ -1,99 +0,0 @@
(function() {
/**
Implementation of the 'analyze' verb for HackMyResume.
@module verbs/analyze
@license MIT. See LICENSE.md for details.
*/
/** Private workhorse for the 'analyze' command. */
/** Analyze a single resume. */
var AnalyzeVerb, HMEVENT, HMSTATUS, MKDIRP, PATH, ResumeFactory, Verb, _, _analyze, _analyzeOne, _loadInspectors, chalk;
MKDIRP = require('mkdirp');
PATH = require('path');
HMEVENT = require('../core/event-codes');
HMSTATUS = require('../core/status-codes');
_ = require('underscore');
ResumeFactory = require('../core/resume-factory');
Verb = require('../verbs/verb');
chalk = require('chalk');
/** An invokable resume analysis command. */
module.exports = AnalyzeVerb = class AnalyzeVerb extends Verb {
constructor() {
super('analyze', _analyze);
}
};
_analyze = function(sources, dst, opts) {
var nlzrs, results;
if (!sources || !sources.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
nlzrs = _loadInspectors();
results = _.map(sources, function(src) {
var r;
r = ResumeFactory.loadOne(src, {
format: 'FRESH',
objectify: true,
inner: {
private: opts.private === true
}
}, this);
if (opts.assert && this.hasError()) {
return {};
}
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
return r;
} else {
return _analyzeOne.call(this, r, nlzrs, opts);
}
}, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
_analyzeOne = function(resumeObject, nlzrs, opts) {
var info, rez, safeFormat;
rez = resumeObject.rez;
safeFormat = rez.meta && rez.meta.format && rez.meta.format.startsWith('FRESH') ? 'FRESH' : 'JRS';
this.stat(HMEVENT.beforeAnalyze, {
fmt: safeFormat,
file: resumeObject.file
});
info = _.mapObject(nlzrs, function(val, key) {
return val.run(rez);
});
this.stat(HMEVENT.afterAnalyze, {
info: info
});
return info;
};
_loadInspectors = function() {
return {
totals: require('../inspectors/totals-inspector'),
coverage: require('../inspectors/gap-inspector'),
keywords: require('../inspectors/keyword-inspector')
};
};
}).call(this);
//# sourceMappingURL=analyze.js.map

484
dist/verbs/build.js vendored
View File

@ -1,484 +0,0 @@
(function() {
/**
Implementation of the 'build' verb for HackMyResume.
@module verbs/build
@license MIT. See LICENSE.md for details.
*/
/**
Reinforce the chosen theme with "freebie" formats provided by HackMyResume.
A "freebie" format is an output format such as JSON, YML, or PNG that can be
generated directly from the resume model or from one of the theme's declared
output formats. For example, the PNG format can be generated for any theme
that declares an HTML format; the theme doesn't have to provide an explicit
PNG template.
@param theTheme A FRESHTheme or JRSTheme object.
*/
/**
Given a source resume in FRESH or JRS format, 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 opts Generation options.
*/
/**
Expand output files. For example, "foo.all" should be expanded to
["foo.html", "foo.doc", "foo.pdf", "etc"].
@param dst An array of output files as specified by the user.
@param theTheme A FRESHTheme or JRSTheme object.
*/
/**
Load the specified theme, which could be either a FRESH theme or a JSON Resume
theme (or both).
*/
/**
Prepare for a BUILD run.
*/
/**
Generate a single target resume such as "out/rez.html" or "out/rez.doc".
TODO: Refactor.
@param targInfo Information for the target resume.
@param theme A FRESHTheme or JRSTheme object.
*/
/** Ensure that user-specified outputs/targets are valid. */
/**
Verify the specified theme name/path.
*/
var BuildVerb, FRESHTheme, FS, HMEVENT, HMSTATUS, JRSTheme, MD, MKDIRP, PATH, RConverter, RTYPES, ResumeFactory, Verb, _, _addFreebieFormats, _build, _err, _expand, _fmts, _loadTheme, _log, _opts, _prep, _rezObj, _single, _verifyOutputs, _verifyTheme, addFreebieFormats, build, expand, extend, loadTheme, parsePath, prep, single, verifyOutputs, verifyTheme;
_ = require('underscore');
PATH = require('path');
FS = require('fs');
MD = require('marked');
MKDIRP = require('mkdirp');
extend = require('extend');
parsePath = require('parse-filepath');
RConverter = require('fresh-jrs-converter');
HMSTATUS = require('../core/status-codes');
HMEVENT = require('../core/event-codes');
RTYPES = {
FRESH: require('../core/fresh-resume'),
JRS: require('../core/jrs-resume')
};
_opts = require('../core/default-options');
FRESHTheme = require('../core/fresh-theme');
JRSTheme = require('../core/jrs-theme');
ResumeFactory = require('../core/resume-factory');
_fmts = require('../core/default-formats');
Verb = require('../verbs/verb');
_err = null;
_log = null;
_rezObj = null;
build = null;
prep = null;
single = null;
verifyOutputs = null;
addFreebieFormats = null;
expand = null;
verifyTheme = null;
loadTheme = null;
/** An invokable resume generation command. */
module.exports = BuildVerb = class BuildVerb extends Verb {
/** Create a new build verb. */
constructor() {
super('build', _build);
}
};
_build = function(src, dst, opts) {
var err, inv, isFRESH, mixed, newEx, orgFormat, problemSheets, results, rez, sheetObjects, sheets, tFolder, targets, theme, toFormat;
if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
_prep.call(this, src, dst, opts);
// Load input resumes as JSON...
sheetObjects = ResumeFactory.load(src, {
format: null,
objectify: false,
quit: true,
inner: {
sort: _opts.sort,
private: _opts.private
}
}, this);
// Explicit check for any resume loading errors...
problemSheets = _.filter(sheetObjects, function(so) {
return so.fluenterror;
});
if (problemSheets && problemSheets.length) {
problemSheets[0].quit = true; // can't go on
this.err(problemSheets[0].fluenterror, problemSheets[0]);
return null;
}
// Get the collection of raw JSON sheets
sheets = sheetObjects.map(function(r) {
return r.json;
});
// Load the theme...
theme = null;
this.stat(HMEVENT.beforeTheme, {
theme: _opts.theme
});
try {
tFolder = _verifyTheme.call(this, _opts.theme);
if (tFolder.fluenterror) {
tFolder.quit = true;
this.err(tFolder.fluenterror, tFolder);
return;
}
theme = _opts.themeObj = _loadTheme(tFolder);
_addFreebieFormats(theme);
} catch (error) {
err = error;
newEx = {
fluenterror: HMSTATUS.themeLoad,
inner: err,
attempted: _opts.theme,
quit: true
};
this.err(HMSTATUS.themeLoad, newEx);
return null;
}
this.stat(HMEVENT.afterTheme, {
theme: theme
});
// Check for invalid outputs...
inv = _verifyOutputs.call(this, dst, theme);
if (inv && inv.length) {
this.err(HMSTATUS.invalidFormat, {
data: inv,
theme: theme,
quit: true
});
return null;
}
//# Merge input resumes, yielding a single source resume...
rez = null;
if (sheets.length > 1) {
isFRESH = !sheets[0].basics;
mixed = _.any(sheets, function(s) {
if (isFRESH) {
return s.basics;
} else {
return !s.basics;
}
});
this.stat(HMEVENT.beforeMerge, {
f: _.clone(sheetObjects),
mixed: mixed
});
if (mixed) {
this.err(HMSTATUS.mixedMerge);
}
rez = _.reduceRight(sheets, function(a, b, idx) {
return extend(true, b, a);
});
this.stat(HMEVENT.afterMerge, {
r: rez
});
} else {
rez = sheets[0];
}
orgFormat = rez.basics ? 'JRS' : 'FRESH';
toFormat = theme.render ? 'JRS' : 'FRESH';
if (toFormat !== orgFormat) {
this.stat(HMEVENT.beforeInlineConvert);
rez = RConverter['to' + toFormat](rez);
this.stat(HMEVENT.afterInlineConvert, {
file: sheetObjects[0].file,
fmt: toFormat
});
}
// Announce the theme
this.stat(HMEVENT.applyTheme, {
r: rez,
theme: theme
});
// Load the resume into a FRESHResume or JRSResume object
_rezObj = new RTYPES[toFormat]().parseJSON(rez, {
private: _opts.private
});
targets = _expand(dst, theme);
// Run the transformation!
_.each(targets, function(t) {
var ref;
if (this.hasError() && opts.assert) {
return {};
}
t.final = _single.call(this, t, theme, targets);
if ((ref = t.final) != null ? ref.fluenterror : void 0) {
t.final.quit = opts.assert;
this.err(t.final.fluenterror, t.final);
}
}, this);
results = {
sheet: _rezObj,
targets: targets,
processed: targets
};
if (this.hasError() && !opts.assert) {
this.reject(results);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
_prep = function(src, dst, opts) {
var that;
// Cherry-pick options //_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
_opts.prettify = opts.prettify === true;
_opts.private = opts.private === true;
_opts.noescape = opts.noescape === true;
_opts.css = opts.css;
_opts.pdf = opts.pdf;
_opts.wrap = opts.wrap || 60;
_opts.stitles = opts.sectionTitles;
_opts.tips = opts.tips;
_opts.errHandler = opts.errHandler;
_opts.noTips = opts.noTips;
_opts.debug = opts.debug;
_opts.sort = opts.sort;
_opts.wkhtmltopdf = opts.wkhtmltopdf;
that = this;
// Set up callbacks for internal generators
_opts.onTransform = function(info) {
that.stat(HMEVENT.afterTransform, info);
};
_opts.beforeWrite = function(info) {
that.stat(HMEVENT.beforeWrite, info);
};
_opts.afterWrite = function(info) {
that.stat(HMEVENT.afterWrite, info);
};
// If two or more files are passed to the GENERATE command and the TO
// keyword is omitted, the last file specifies the output file.
(src.length > 1 && (!dst || !dst.length)) && dst.push(src.pop());
};
_single = function(targInfo, theme, finished) {
var e, ex, f, fName, fType, outFolder, ret, theFormat;
ret = null;
ex = null;
f = targInfo.file;
try {
if (!targInfo.fmt) {
return {};
}
fType = targInfo.fmt.outFormat;
fName = PATH.basename(f, '.' + fType);
theFormat = null;
this.stat(HMEVENT.beforeGenerate, {
fmt: targInfo.fmt.outFormat,
file: PATH.relative(process.cwd(), f)
});
_opts.targets = finished;
// If targInfo.fmt.files exists, this format is backed by a document.
// Fluent/FRESH themes are handled here.
if (targInfo.fmt.files && targInfo.fmt.files.length) {
theFormat = _fmts.filter(function(fmt) {
return fmt.name === targInfo.fmt.outFormat;
})[0];
MKDIRP.sync(PATH.dirname(f));
ret = theFormat.gen.generate(_rezObj, f, _opts);
} else {
// Otherwise this is an ad-hoc format (JSON, YML, or PNG) that every theme
// gets "for free".
theFormat = _fmts.filter(function(fmt) {
return fmt.name === targInfo.fmt.outFormat;
})[0];
outFolder = PATH.dirname(f);
MKDIRP.sync(outFolder); // Ensure dest folder exists;
ret = theFormat.gen.generate(_rezObj, f, _opts);
}
} catch (error) {
e = error;
ex = e;
}
this.stat(HMEVENT.afterGenerate, {
fmt: targInfo.fmt.outFormat,
file: PATH.relative(process.cwd(), f),
error: ex
});
if (ex) {
if (ex.fluenterror) {
ret = ex;
} else {
ret = {
fluenterror: HMSTATUS.generateError,
inner: ex
};
}
}
return ret;
};
_verifyOutputs = function(targets, theme) {
this.stat(HMEVENT.verifyOutputs, {
targets: targets,
theme: theme
});
return _.reject(targets.map(function(t) {
var pathInfo;
pathInfo = parsePath(t);
return {
format: pathInfo.extname.substr(1)
};
}), function(t) {
return t.format === 'all' || theme.hasFormat(t.format);
});
};
_addFreebieFormats = function(theTheme) {
// Add freebie formats (JSON, YAML, PNG) every theme gets...
// Add HTML-driven PNG only if the theme has an HTML format.
theTheme.formats.json = theTheme.formats.json || {
freebie: true,
title: 'json',
outFormat: 'json',
pre: 'json',
ext: 'json',
path: null,
data: null
};
theTheme.formats.yml = theTheme.formats.yml || {
freebie: true,
title: 'yaml',
outFormat: 'yml',
pre: 'yml',
ext: 'yml',
path: null,
data: null
};
if (theTheme.formats.html && !theTheme.formats.png) {
theTheme.formats.png = {
freebie: true,
title: 'png',
outFormat: 'png',
ext: 'yml',
path: null,
data: null
};
}
};
_expand = function(dst, theTheme) {
var destColl, targets;
// Set up the destination collection. It's either the array of files passed
// by the user or 'out/resume.all' if no targets were specified.
destColl = (dst && dst.length && dst) || [PATH.normalize('out/resume.all')];
targets = [];
destColl.forEach(function(t) {
var fmat, pa, to;
to = PATH.resolve(t);
pa = parsePath(to);
fmat = pa.extname || '.all';
return targets.push.apply(targets, fmat === '.all' ? Object.keys(theTheme.formats).map(function(k) {
var z;
z = theTheme.formats[k];
return {
file: to.replace(/all$/g, z.outFormat),
fmt: z
};
}) : [
{
file: to,
fmt: theTheme.getFormat(fmat.slice(1))
}
]);
});
return targets;
};
_verifyTheme = function(themeNameOrPath) {
var exists, tFolder, themesObj;
// First, see if this is one of the predefined FRESH themes. There are only a
// handful of these, but they may change over time, so we need to query
// the official source of truth: the fresh-themes repository, which mounts the
// themes conveniently by name to the module object, and which is embedded
// locally inside the HackMyResume installation.
themesObj = require('fresh-themes');
if (_.has(themesObj.themes, themeNameOrPath)) {
tFolder = PATH.join(parsePath(require.resolve('fresh-themes')).dirname, '/themes/', themeNameOrPath);
} else {
// Otherwsie it's a path to an arbitrary FRESH or JRS theme sitting somewhere
// on the user's system (or, in the future, at a URI).
tFolder = PATH.resolve(themeNameOrPath);
}
// In either case, make sure the theme folder exists
exists = require('path-exists').sync;
if (exists(tFolder)) {
return tFolder;
} else {
return {
fluenterror: HMSTATUS.themeNotFound,
data: _opts.theme
};
}
};
_loadTheme = function(tFolder) {
var exists, theTheme, themeJsonPath;
themeJsonPath = PATH.join(tFolder, 'theme.json'); // [^1]
exists = require('path-exists').sync;
// Create a FRESH or JRS theme object
theTheme = exists(themeJsonPath) ? new FRESHTheme().open(tFolder) : new JRSTheme().open(tFolder);
// Cache the theme object
_opts.themeObj = theTheme;
return theTheme;
};
// FOOTNOTES
// ------------------------------------------------------------------------------
// [^1] We don't know ahead of time whether this is a FRESH or JRS theme.
// However, all FRESH themes have a theme.json file, so we'll use that as a
// canary for now, as an interim solution.
// Unfortunately, with the exception of FRESH's theme.json, both FRESH and
// JRS themes are free-form and don't have a ton of reliable distinguishing
// marks, which makes a simple task like ad hoc theme detection harder than
// it should be to do cleanly.
// Another complicating factor is that it's possible for a theme to be BOTH.
// That is, a single set of theme files can serve as a FRESH theme -and- a
// JRS theme.
// TODO: The most robust way to deal with all these issues is with a strong
// theme validator. If a theme structure validates as a particular theme
// type, then for all intents and purposes, it IS a theme of that type.
}).call(this);
//# sourceMappingURL=build.js.map

166
dist/verbs/convert.js vendored
View File

@ -1,166 +0,0 @@
(function() {
/**
Implementation of the 'convert' verb for HackMyResume.
@module verbs/convert
@license MIT. See LICENSE.md for details.
*/
/** Private workhorse method. Convert 0..N resumes between FRESH and JRS
formats. */
/** Private workhorse method. Convert a single resume. */
var ConvertVerb, HMEVENT, HMSTATUS, ResumeFactory, Verb, _, _convert, _convertOne, chalk;
ResumeFactory = require('../core/resume-factory');
chalk = require('chalk');
Verb = require('../verbs/verb');
HMSTATUS = require('../core/status-codes');
_ = require('underscore');
HMEVENT = require('../core/event-codes');
module.exports = ConvertVerb = class ConvertVerb extends Verb {
constructor() {
super('convert', _convert);
}
};
_convert = function(srcs, dst, opts) {
var fmtUp, results, targetVer;
if (!srcs || !srcs.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
if (!dst || !dst.length) {
if (srcs.length === 1) {
this.err(HMSTATUS.inputOutputParity, {
quit: true
});
} else if (srcs.length === 2) {
dst = dst || [];
dst.push(srcs.pop());
} else {
this.err(HMSTATUS.inputOutputParity, {
quit: true
});
}
}
// Different number of source and dest resumes? Error out.
if (srcs && dst && srcs.length && dst.length && srcs.length !== dst.length) {
this.err(HMSTATUS.inputOutputParity, {
quit: true
});
}
// Validate the destination format (if specified)
targetVer = null;
if (opts.format) {
fmtUp = opts.format.trim().toUpperCase();
if (!_.contains(['FRESH', 'FRESCA', 'JRS', 'JRS@1', 'JRS@edge'], fmtUp)) {
this.err(HMSTATUS.invalidSchemaVersion, {
data: opts.format.trim(),
quit: true
});
}
}
// freshVerRegex = require '../utils/fresh-version-regex'
// matches = fmtUp.match freshVerRegex()
// # null
// # [ 'JRS@1.0', 'JRS', '1.0', index: 0, input: 'FRESH' ]
// # [ 'FRESH', 'FRESH', undefined, index: 0, input: 'FRESH' ]
// if not matches
// @err HMSTATUS.invalidSchemaVersion, data: opts.format.trim(), quit: true
// targetSchema = matches[1]
// targetVer = matches[2] || '1'
// If any errors have occurred this early, we're done.
if (this.hasError()) {
this.reject(this.errorCode);
return null;
}
// Map each source resume to the converted destination resume
results = _.map(srcs, function(src, idx) {
var r;
// Convert each resume in turn
r = _convertOne.call(this, src, dst, idx, fmtUp);
// Handle conversion errors
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
}
return r;
}, this);
if (this.hasError() && !opts.assert) {
this.reject(results);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
_convertOne = function(src, dst, idx, targetSchema) {
var err, rez, rinfo, srcFmt, targetFormat;
// Load the resume
rinfo = ResumeFactory.loadOne(src, {
format: null,
objectify: true,
inner: {
privatize: false
}
});
// If a load error occurs, report it and move on to the next file (if any)
if (rinfo.fluenterror) {
this.stat(HMEVENT.beforeConvert, {
srcFile: src, //rinfo.file
srcFmt: '???',
dstFile: dst[idx],
dstFmt: '???',
error: true
});
//@err rinfo.fluenterror, rinfo
return rinfo;
}
// Determine the resume's SOURCE format
// TODO: replace with detector component
rez = rinfo.rez;
srcFmt = '';
if (rez.meta && rez.meta.format) { //&& rez.meta.format.substr(0, 5).toUpperCase() == 'FRESH'
srcFmt = 'FRESH';
} else if (rez.basics) {
srcFmt = 'JRS';
} else {
rinfo.fluenterror = HMSTATUS.unknownSchema;
return rinfo;
}
// Determine the TARGET format for the conversion
targetFormat = targetSchema || (srcFmt === 'JRS' ? 'FRESH' : 'JRS');
// Fire the beforeConvert event
this.stat(HMEVENT.beforeConvert, {
srcFile: rinfo.file,
srcFmt: srcFmt,
dstFile: dst[idx],
dstFmt: targetFormat
});
try {
// Save it to the destination format
rez.saveAs(dst[idx], targetFormat);
} catch (error) {
err = error;
if (err.badVer) {
return {
fluenterror: HMSTATUS.invalidSchemaVersion,
quit: true,
data: err.badVer
};
}
}
return rez;
};
}).call(this);
//# sourceMappingURL=convert.js.map

92
dist/verbs/create.js vendored
View File

@ -1,92 +0,0 @@
(function() {
/**
Implementation of the 'create' verb for HackMyResume.
@module verbs/create
@license MIT. See LICENSE.md for details.
*/
/** Create a new empty resume in either FRESH or JRS format. */
/** Create a single new resume */
var CreateVerb, HMEVENT, HMSTATUS, MKDIRP, PATH, Verb, _, _create, _createOne, chalk;
MKDIRP = require('mkdirp');
PATH = require('path');
chalk = require('chalk');
Verb = require('../verbs/verb');
_ = require('underscore');
HMSTATUS = require('../core/status-codes');
HMEVENT = require('../core/event-codes');
module.exports = CreateVerb = class CreateVerb extends Verb {
constructor() {
super('new', _create);
}
};
_create = function(src, dst, opts) {
var results;
if (!src || !src.length) {
this.err(HMSTATUS.createNameMissing, {
quit: true
});
return null;
}
results = _.map(src, function(t) {
var r;
if (opts.assert && this.hasError()) {
return {};
}
r = _createOne.call(this, t, opts);
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
}
return r;
}, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
_createOne = function(t, opts) {
var RezClass, err, newRez, ret, safeFmt;
try {
ret = null;
safeFmt = opts.format.toUpperCase();
this.stat(HMEVENT.beforeCreate, {
fmt: safeFmt,
file: t
});
MKDIRP.sync(PATH.dirname(t)); // Ensure dest folder exists;
RezClass = require('../core/' + safeFmt.toLowerCase() + '-resume');
newRez = RezClass.default();
newRez.save(t);
ret = newRez;
} catch (error) {
err = error;
ret = {
fluenterror: HMSTATUS.createError,
inner: err
};
} finally {
this.stat(HMEVENT.afterCreate, {
fmt: safeFmt,
file: t,
isError: ret.fluenterror
});
return ret;
}
};
}).call(this);
//# sourceMappingURL=create.js.map

100
dist/verbs/peek.js vendored
View File

@ -1,100 +0,0 @@
(function() {
/**
Implementation of the 'peek' verb for HackMyResume.
@module verbs/peek
@license MIT. See LICENSE.md for details.
*/
/** Peek at a resume, resume section, or resume field. */
/** Peek at a single resume, resume section, or resume field. */
var HMEVENT, HMSTATUS, PeekVerb, Verb, _, __, _peek, _peekOne, safeLoadJSON;
Verb = require('../verbs/verb');
_ = require('underscore');
__ = require('lodash');
safeLoadJSON = require('../utils/safe-json-loader');
HMSTATUS = require('../core/status-codes');
HMEVENT = require('../core/event-codes');
module.exports = PeekVerb = class PeekVerb extends Verb {
constructor() {
super('peek', _peek);
}
};
_peek = function(src, dst, opts) {
var objPath, results;
if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
objPath = (dst && dst[0]) || '';
results = _.map(src, function(t) {
var tgt;
if (opts.assert && this.hasError()) {
return {};
}
tgt = _peekOne.call(this, t, objPath);
if (tgt.error) {
this.setError(tgt.error.fluenterror, tgt.error);
}
//tgt.error.quit = opts.assert
//@err tgt.error.fluenterror, tgt.error
return tgt;
}, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
_peekOne = function(t, objPath) {
var errCode, obj, pkgError, tgt;
this.stat(HMEVENT.beforePeek, {
file: t,
target: objPath
});
// Load the input file JSON 1st
obj = safeLoadJSON(t);
// Fetch the requested object path (or the entire file)
tgt = null;
if (!obj.ex) {
tgt = objPath ? __.get(obj.json, objPath) : obj.json;
}
//# safeLoadJSON can only return a READ error or a PARSE error
pkgError = null;
if (obj.ex) {
errCode = obj.ex.op === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError;
if (errCode === HMSTATUS.readError) {
obj.ex.quiet = true;
}
pkgError = {
fluenterror: errCode,
inner: obj.ex
};
}
// Fire the 'afterPeek' event with collected info
this.stat(HMEVENT.afterPeek, {
file: t,
requested: objPath,
target: obj.ex ? void 0 : tgt,
error: pkgError
});
return {
val: obj.ex ? void 0 : tgt,
error: pkgError
};
};
}).call(this);
//# sourceMappingURL=peek.js.map

132
dist/verbs/validate.js vendored
View File

@ -1,132 +0,0 @@
(function() {
/**
Implementation of the 'validate' verb for HackMyResume.
@module verbs/validate
@license MIT. See LICENSE.md for details.
*/
/**
Validate a single resume.
@returns {
file: <fileName>,
isValid: <validFlag>,
status: <validationStatus>,
violations: <validationErrors>,
schema: <schemaType>,
error: <errorObject>
}
*/
var FS, HMEVENT, HMSTATUS, ResumeFactory, SyntaxErrorEx, ValidateVerb, Verb, _, _validate, _validateOne, chalk, safeLoadJSON;
FS = require('fs');
ResumeFactory = require('../core/resume-factory');
SyntaxErrorEx = require('../utils/syntax-error-ex');
chalk = require('chalk');
Verb = require('../verbs/verb');
HMSTATUS = require('../core/status-codes');
HMEVENT = require('../core/event-codes');
_ = require('underscore');
safeLoadJSON = require('../utils/safe-json-loader');
/** An invokable resume validation command. */
module.exports = ValidateVerb = class ValidateVerb extends Verb {
constructor() {
super('validate', _validate);
}
};
// Validate 1 to N resumes in FRESH or JSON Resume format.
_validate = function(sources, unused, opts) {
var results, schemas, validator;
if (!sources || !sources.length) {
this.err(HMSTATUS.resumeNotFoundAlt, {
quit: true
});
return null;
}
validator = require('is-my-json-valid');
schemas = {
fresh: require('fresh-resume-schema'),
jars: require('../core/resume.json')
};
results = _.map(sources, function(t) {
var r;
r = _validateOne.call(this, t, validator, schemas, opts);
if (r.error) {
this.err(r.error.fluenterror, r.error);
}
return r;
}, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
_validateOne = function(t, validator, schemas, opts) {
var err, errCode, obj, ret, validate;
ret = {
file: t,
isValid: false,
status: 'unknown',
schema: '-----'
};
try {
// Read and parse the resume JSON. Won't throw.
obj = safeLoadJSON(t);
if (!obj.ex) {
if (obj.json.basics) {
ret.schema = 'jars';
} else {
ret.schema = 'fresh';
}
validate = validator(schemas[ret.schema], {
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
ret.isValid = validate(obj.json);
ret.status = ret.isValid ? 'valid' : 'invalid';
if (!ret.isValid) {
ret.violations = validate.errors;
}
} else {
// If failure, package JSON read/parse errors
if (obj.ex.op === 'parse') {
errCode = HMSTATUS.parseError;
ret.status = 'broken';
} else {
errCode = HMSTATUS.readError;
ret.status = 'missing';
}
ret.error = {
fluenterror: errCode,
inner: obj.ex.inner,
quiet: errCode === HMSTATUS.readError
};
}
} catch (error) {
err = error;
// Package any unexpected exceptions
ret.error = {
fluenterror: HMSTATUS.validateError,
inner: err
};
}
this.stat(HMEVENT.afterValidate, ret);
return ret;
};
}).call(this);
//# sourceMappingURL=validate.js.map

100
dist/verbs/verb.js vendored
View File

@ -1,100 +0,0 @@
(function() {
/**
Definition of the Verb class.
@module verbs/verb
@license MIT. See LICENSE.md for details.
*/
var EVENTS, HMEVENT, Promise, Verb;
EVENTS = require('events');
HMEVENT = require('../core/event-codes');
Promise = require('pinkie-promise');
/**
An abstract invokable verb.
Provides base class functionality for verbs. Provide common services such as
error handling, event management, and promise support.
@class Verb
*/
module.exports = Verb = class Verb {
/** Constructor. Automatically called at creation. */
constructor(moniker, workhorse) {
this.moniker = moniker;
this.workhorse = workhorse;
this.emitter = new EVENTS.EventEmitter();
return;
}
/** Invoke the command. */
invoke() {
var argsArray, that;
// Sent the 'begin' notification for this verb
this.stat(HMEVENT.begin, {
cmd: this.moniker
});
// Prepare command arguments
argsArray = Array.prototype.slice.call(arguments);
// Create a promise for this verb instance
that = this;
return this.promise = new Promise(function(res, rej) {
that.resolve = res;
that.reject = rej;
that.workhorse.apply(that, argsArray);
});
}
/** Forward subscriptions to the event emitter. */
on() {
return this.emitter.on.apply(this.emitter, arguments);
}
/** Fire an arbitrary event, scoped to "hmr:". */
fire(evtName, payload) {
payload = payload || {};
payload.cmd = this.moniker;
this.emitter.emit('hmr:' + evtName, payload);
return true;
}
/** Handle an error condition. */
err(errorCode, payload, hot) {
payload = payload || {};
payload.sub = payload.fluenterror = errorCode;
payload.throw = hot;
this.setError(errorCode, payload);
if (payload.quit) {
this.reject(errorCode);
}
this.fire('error', payload);
if (hot) {
throw payload;
}
return true;
}
/** Fire the 'hmr:status' error event. */
stat(subEvent, payload) {
payload = payload || {};
payload.sub = subEvent;
this.fire('status', payload);
return true;
}
/** Has an error occurred during this verb invocation? */
hasError() {
return this.errorCode || this.errorObj;
}
/** Associate error info with the invocation. */
setError(code, obj) {
this.errorCode = code;
this.errorObj = obj;
}
};
}).call(this);
//# sourceMappingURL=verb.js.map

View File

@ -44,9 +44,9 @@
"url": "https://github.com/hacksalot/HackMyResume/issues"
},
"bin": {
"hackmyresume": "dist/cli/index.js"
"hackmyresume": "src/cli/index.js"
},
"main": "dist/index.js",
"main": "src/index.js",
"homepage": "https://github.com/hacksalot/HackMyResume",
"dependencies": {
"chalk": "^2.3.1",

View File

@ -1,268 +0,0 @@
###*
Error-handling routines for HackMyResume.
@module cli/error
@license MIT. See LICENSE.md for details.
###
HMSTATUS = require '../core/status-codes'
FS = require 'fs'
PATH = require 'path'
WRAP = require 'word-wrap'
M2C = require '../utils/md2chalk'
chalk = require 'chalk'
extend = require 'extend'
printf = require 'printf'
SyntaxErrorEx = require '../utils/syntax-error-ex'
require 'string.prototype.startswith'
###* Error handler for HackMyResume. All errors are handled here.
@class ErrorHandler ###
module.exports =
init: ( debug, assert, silent ) ->
@debug = debug
@assert = assert
@silent = silent
@msgs = require('./msg').errors
@
err: ( ex, shouldExit ) ->
# Short-circuit logging output if --silent is on
o = if @silent then () -> else _defaultLog
# Special case; can probably be removed.
throw ex if ex.pass
# Load error messages
@msgs = @msgs || require('./msg').errors
# Handle packaged HMR exceptions
if ex.fluenterror
# Output the error message
objError = assembleError.call @, ex
o( @[ 'format_' + objError.etype ]( objError.msg ))
# Output the stack (sometimes)
if objError.withStack
stack = ex.stack || (ex.inner && ex.inner.stack);
stack && o( chalk.gray( stack ) );
# Quit if necessary
if shouldExit or ex.exit
if @debug
o chalk.cyan('Exiting with error code ' + ex.fluenterror.toString())
if @assert
ex.pass = true
throw ex
process.exit ex.fluenterror
# Handle raw exceptions
else
o ex
stackTrace = ex.stack || (ex.inner && ex.inner.stack)
if stackTrace && this.debug
o M2C(ex.stack || ex.inner.stack, 'gray')
format_error: ( msg ) ->
msg = msg || ''
chalk.red.bold( if msg.toUpperCase().startsWith('ERROR:') then msg else 'Error: ' + msg )
format_warning: ( brief, msg ) ->
chalk.yellow(brief) + chalk.yellow(msg || '')
format_custom: ( msg ) -> msg
_defaultLog = () -> console.log.apply console.log, arguments # eslint-disable-line no-console
assembleError = ( ex ) ->
msg = ''
withStack = false
quit = false
etype = 'warning'
withStack = true if @debug
switch ex.fluenterror
when HMSTATUS.themeNotFound
msg = printf( M2C( this.msgs.themeNotFound.msg, 'yellow' ), ex.data)
when HMSTATUS.copyCSS
msg = M2C( this.msgs.copyCSS.msg, 'red' )
quit = false
when HMSTATUS.resumeNotFound
#msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' );
msg += M2C(FS.readFileSync(
PATH.resolve(__dirname, 'help/' + ex.verb + '.txt'), 'utf8' ), 'white', 'yellow')
when HMSTATUS.missingCommand
# msg = M2C( this.msgs.missingCommand.msg + " (", 'yellow');
# msg += Object.keys( FCMD.verbs ).map( (v, idx, ar) ->
# return ( if idx == ar.length - 1 then chalk.yellow('or ') else '') +
# chalk.yellow.bold(v.toUpperCase());
# ).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += M2C(FS.readFileSync(
PATH.resolve(__dirname, 'help/use.txt'), 'utf8' ), 'white', 'yellow')
when HMSTATUS.invalidCommand
msg = printf( M2C( this.msgs.invalidCommand.msg, 'yellow'), ex.attempted )
when HMSTATUS.resumeNotFoundAlt
msg = M2C( this.msgs.resumeNotFoundAlt.msg, 'yellow' )
when HMSTATUS.inputOutputParity
msg = M2C( this.msgs.inputOutputParity.msg )
when HMSTATUS.createNameMissing
msg = M2C( this.msgs.createNameMissing.msg )
when HMSTATUS.pdfGeneration
msg = M2C( this.msgs.pdfGeneration.msg, 'bold' )
msg += chalk.red('\n' + ex.inner) if ex.inner
quit = false
etype = 'error'
when HMSTATUS.invalid
msg = M2C( this.msgs.invalid.msg, 'red' )
etype = 'error'
when HMSTATUS.generateError
msg = (ex.inner && ex.inner.toString()) || ex
quit = false
etype = 'error'
when HMSTATUS.fileSaveError
msg = printf( M2C( this.msgs.fileSaveError.msg ), (ex.inner || ex).toString() )
etype = 'error'
quit = false
when HMSTATUS.invalidFormat
ex.data.forEach( (d) ->
msg += printf( M2C( this.msgs.invalidFormat.msg, 'bold' ),
ex.theme.name.toUpperCase(), d.format.toUpperCase())
, @);
when HMSTATUS.missingParam
msg = printf(M2C( this.msgs.missingParam.msg ), ex.expected, ex.helper)
when HMSTATUS.invalidHelperUse
msg = printf( M2C( this.msgs.invalidHelperUse.msg ), ex.helper )
if ex.error
msg += '\n--> ' + assembleError.call( this, extend( true, {}, ex, {fluenterror: ex.error} )).msg;
#msg += printf( '\n--> ' + M2C( this.msgs.invalidParamCount.msg ), ex.expected );
quit = false
etype = 'warning'
when HMSTATUS.notOnPath
msg = printf( M2C(this.msgs.notOnPath.msg, 'bold'), ex.engine)
quit = false
etype = 'error'
when HMSTATUS.readError
if !ex.quiet
# eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file))
msg = ex.inner.toString()
etype = 'error'
when HMSTATUS.mixedMerge
msg = M2C this.msgs.mixedMerge.msg
quit = false
when HMSTATUS.invokeTemplate
msg = M2C this.msgs.invokeTemplate.msg, 'red'
msg += M2C( '\n' + WRAP(ex.inner.toString(), { width: 60, indent: ' ' }), 'gray' );
etype = 'custom'
when HMSTATUS.compileTemplate
etype = 'error'
when HMSTATUS.themeLoad
msg = M2C( printf( this.msgs.themeLoad.msg, ex.attempted.toUpperCase() ), 'red');
if ex.inner && ex.inner.fluenterror
msg += M2C('\nError: ', 'red') + assembleError.call( this, ex.inner ).msg
quit = true
etype = 'custom'
when HMSTATUS.parseError
if SyntaxErrorEx.is ex.inner
# eslint-disable-next-line no-console
console.error printf( M2C(this.msgs.readError.msg, 'red'), ex.file )
se = new SyntaxErrorEx ex, ex.raw
if se.line? and se.col?
msg = printf M2C( this.msgs.parseError.msg[0], 'red' ), se.line, se.col
else if se.line?
msg = printf M2C( this.msgs.parseError.msg[1], 'red' ), se.line
else
msg = M2C @msgs.parseError.msg[2], 'red'
else if ex.inner && ex.inner.line? && ex.inner.col?
msg = printf( M2C( this.msgs.parseError.msg[0], 'red' ), ex.inner.line, ex.inner.col)
else
msg = ex
etype = 'error'
when HMSTATUS.createError
# inner.code could be EPERM, EACCES, etc
msg = printf M2C( this.msgs.createError.msg ), ex.inner.path
etype = 'error'
when HMSTATUS.validateError
msg = printf M2C( @msgs.validateError.msg ), ex.inner.toString()
etype = 'error'
when HMSTATUS.invalidOptionsFile
msg = M2C @msgs.invalidOptionsFile.msg[0]
if SyntaxErrorEx.is ex.inner
# eslint-disable-next-line no-console
console.error printf( M2C(this.msgs.readError.msg, 'red'), ex.file )
se = new SyntaxErrorEx ex, ex.raw
if se.line? and se.col?
msg += printf M2C( this.msgs.parseError.msg[0], 'red' ), se.line, se.col
else if se.line?
msg += printf M2C( this.msgs.parseError.msg[1], 'red' ), se.line
else
msg += M2C @msgs.parseError.msg[2], 'red'
else if ex.inner && ex.inner.line? && ex.inner.col?
msg += printf( M2C( this.msgs.parseError.msg[0], 'red' ), ex.inner.line, ex.inner.col)
else
msg += ex
msg += @msgs.invalidOptionsFile.msg[1]
etype = 'error'
when HMSTATUS.optionsFileNotFound
msg = M2C( @msgs.optionsFileNotFound.msg )
etype = 'error'
when HMSTATUS.unknownSchema
msg = M2C( @msgs.unknownSchema.msg[0] )
#msg += "\n" + M2C( @msgs.unknownSchema.msg[1], 'yellow' )
etype = 'error'
when HMSTATUS.themeHelperLoad
msg = printf M2C( @msgs.themeHelperLoad.msg ), ex.glob
etype = 'error'
when HMSTATUS.invalidSchemaVersion
msg = printf M2C( @msgs.invalidSchemaVersion.msg ), ex.data
etype = 'error'
msg: msg # The error message to display
withStack: withStack # Whether to include the stack
quit: quit
etype: etype

328
src/cli/error.js Normal file
View File

@ -0,0 +1,328 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Error-handling routines for HackMyResume.
@module cli/error
@license MIT. See LICENSE.md for details.
*/
const HMSTATUS = require('../core/status-codes');
const FS = require('fs');
const PATH = require('path');
const WRAP = require('word-wrap');
const M2C = require('../utils/md2chalk');
const chalk = require('chalk');
const extend = require('extend');
const printf = require('printf');
const SyntaxErrorEx = require('../utils/syntax-error-ex');
require('string.prototype.startswith');
/** Error handler for HackMyResume. All errors are handled here.
@class ErrorHandler */
module.exports = {
init( debug, assert, silent ) {
this.debug = debug;
this.assert = assert;
this.silent = silent;
this.msgs = require('./msg').errors;
return this;
},
err( ex, shouldExit ) {
// Short-circuit logging output if --silent is on
let stack;
const o = this.silent ? function() {} : _defaultLog;
// Special case; can probably be removed.
if (ex.pass) { throw ex; }
// Load error messages
this.msgs = this.msgs || require('./msg').errors;
// Handle packaged HMR exceptions
if (ex.fluenterror) {
// Output the error message
const objError = assembleError.call(this, ex);
o( this[ `format_${objError.etype}` ]( objError.msg ));
// Output the stack (sometimes)
if (objError.withStack) {
stack = ex.stack || (ex.inner && ex.inner.stack);
stack && o( chalk.gray( stack ) );
}
// Quit if necessary
if (shouldExit || ex.exit) {
if (this.debug) {
o(chalk.cyan(`Exiting with error code ${ex.fluenterror.toString()}`));
}
if (this.assert) {
ex.pass = true;
throw ex;
}
return process.exit(ex.fluenterror);
}
// Handle raw exceptions
} else {
o(ex);
const stackTrace = ex.stack || (ex.inner && ex.inner.stack);
if (stackTrace && this.debug) {
return o(M2C(ex.stack || ex.inner.stack, 'gray'));
}
}
},
format_error( msg ) {
msg = msg || '';
return chalk.red.bold( msg.toUpperCase().startsWith('ERROR:') ? msg : `Error: ${msg}` );
},
format_warning( brief, msg ) {
return chalk.yellow(brief) + chalk.yellow(msg || '');
},
format_custom( msg ) { return msg; }
};
var _defaultLog = function() { return console.log.apply(console.log, arguments); }; // eslint-disable-line no-console
var assembleError = function( ex ) {
let se;
let msg = '';
let withStack = false;
let quit = false;
let etype = 'warning';
if (this.debug) { withStack = true; }
switch (ex.fluenterror) {
case HMSTATUS.themeNotFound:
msg = printf( M2C( this.msgs.themeNotFound.msg, 'yellow' ), ex.data);
break;
case HMSTATUS.copyCSS:
msg = M2C( this.msgs.copyCSS.msg, 'red' );
quit = false;
break;
case HMSTATUS.resumeNotFound:
//msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' );
msg += M2C(FS.readFileSync(
PATH.resolve(__dirname, `help/${ex.verb}.txt`), 'utf8' ), 'white', 'yellow');
break;
case HMSTATUS.missingCommand:
// msg = M2C( this.msgs.missingCommand.msg + " (", 'yellow');
// msg += Object.keys( FCMD.verbs ).map( (v, idx, ar) ->
// return ( if idx == ar.length - 1 then chalk.yellow('or ') else '') +
// chalk.yellow.bold(v.toUpperCase());
// ).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += M2C(FS.readFileSync(
PATH.resolve(__dirname, 'help/use.txt'), 'utf8' ), 'white', 'yellow');
break;
case HMSTATUS.invalidCommand:
msg = printf( M2C( this.msgs.invalidCommand.msg, 'yellow'), ex.attempted );
break;
case HMSTATUS.resumeNotFoundAlt:
msg = M2C( this.msgs.resumeNotFoundAlt.msg, 'yellow' );
break;
case HMSTATUS.inputOutputParity:
msg = M2C( this.msgs.inputOutputParity.msg );
break;
case HMSTATUS.createNameMissing:
msg = M2C( this.msgs.createNameMissing.msg );
break;
case HMSTATUS.pdfGeneration:
msg = M2C( this.msgs.pdfGeneration.msg, 'bold' );
if (ex.inner) { msg += chalk.red(`\n${ex.inner}`); }
quit = false;
etype = 'error';
break;
case HMSTATUS.invalid:
msg = M2C( this.msgs.invalid.msg, 'red' );
etype = 'error';
break;
case HMSTATUS.generateError:
msg = (ex.inner && ex.inner.toString()) || ex;
quit = false;
etype = 'error';
break;
case HMSTATUS.fileSaveError:
msg = printf( M2C( this.msgs.fileSaveError.msg ), (ex.inner || ex).toString() );
etype = 'error';
quit = false;
break;
case HMSTATUS.invalidFormat:
ex.data.forEach( function(d) {
return msg += printf( M2C( this.msgs.invalidFormat.msg, 'bold' ),
ex.theme.name.toUpperCase(), d.format.toUpperCase());
}
, this);
break;
case HMSTATUS.missingParam:
msg = printf(M2C( this.msgs.missingParam.msg ), ex.expected, ex.helper);
break;
case HMSTATUS.invalidHelperUse:
msg = printf( M2C( this.msgs.invalidHelperUse.msg ), ex.helper );
if (ex.error) {
msg += `\n--> ${assembleError.call( this, extend( true, {}, ex, {fluenterror: ex.error} )).msg}`;
}
//msg += printf( '\n--> ' + M2C( this.msgs.invalidParamCount.msg ), ex.expected );
quit = false;
etype = 'warning';
break;
case HMSTATUS.notOnPath:
msg = printf( M2C(this.msgs.notOnPath.msg, 'bold'), ex.engine);
quit = false;
etype = 'error';
break;
case HMSTATUS.readError:
if (!ex.quiet) {
// eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file));
}
msg = ex.inner.toString();
etype = 'error';
break;
case HMSTATUS.mixedMerge:
msg = M2C(this.msgs.mixedMerge.msg);
quit = false;
break;
case HMSTATUS.invokeTemplate:
msg = M2C(this.msgs.invokeTemplate.msg, 'red');
msg += M2C( `\n${WRAP(ex.inner.toString(), { width: 60, indent: ' ' })}`, 'gray' );
etype = 'custom';
break;
case HMSTATUS.compileTemplate:
etype = 'error';
break;
case HMSTATUS.themeLoad:
msg = M2C( printf( this.msgs.themeLoad.msg, ex.attempted.toUpperCase() ), 'red');
if (ex.inner && ex.inner.fluenterror) {
msg += M2C('\nError: ', 'red') + assembleError.call( this, ex.inner ).msg;
}
quit = true;
etype = 'custom';
break;
case HMSTATUS.parseError:
if (SyntaxErrorEx.is(ex.inner)) {
// eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file ));
se = new SyntaxErrorEx(ex, ex.raw);
if ((se.line != null) && (se.col != null)) {
msg = printf(M2C( this.msgs.parseError.msg[0], 'red' ), se.line, se.col);
} else if (se.line != null) {
msg = printf(M2C( this.msgs.parseError.msg[1], 'red' ), se.line);
} else {
msg = M2C(this.msgs.parseError.msg[2], 'red');
}
} else if (ex.inner && (ex.inner.line != null) && (ex.inner.col != null)) {
msg = printf( M2C( this.msgs.parseError.msg[0], 'red' ), ex.inner.line, ex.inner.col);
} else {
msg = ex;
}
etype = 'error';
break;
case HMSTATUS.createError:
// inner.code could be EPERM, EACCES, etc
msg = printf(M2C( this.msgs.createError.msg ), ex.inner.path);
etype = 'error';
break;
case HMSTATUS.validateError:
msg = printf(M2C( this.msgs.validateError.msg ), ex.inner.toString());
etype = 'error';
break;
case HMSTATUS.invalidOptionsFile:
msg = M2C(this.msgs.invalidOptionsFile.msg[0]);
if (SyntaxErrorEx.is(ex.inner)) {
// eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file ));
se = new SyntaxErrorEx(ex, ex.raw);
if ((se.line != null) && (se.col != null)) {
msg += printf(M2C( this.msgs.parseError.msg[0], 'red' ), se.line, se.col);
} else if (se.line != null) {
msg += printf(M2C( this.msgs.parseError.msg[1], 'red' ), se.line);
} else {
msg += M2C(this.msgs.parseError.msg[2], 'red');
}
} else if (ex.inner && (ex.inner.line != null) && (ex.inner.col != null)) {
msg += printf( M2C( this.msgs.parseError.msg[0], 'red' ), ex.inner.line, ex.inner.col);
} else {
msg += ex;
}
msg += this.msgs.invalidOptionsFile.msg[1];
etype = 'error';
break;
case HMSTATUS.optionsFileNotFound:
msg = M2C( this.msgs.optionsFileNotFound.msg );
etype = 'error';
break;
case HMSTATUS.unknownSchema:
msg = M2C( this.msgs.unknownSchema.msg[0] );
//msg += "\n" + M2C( @msgs.unknownSchema.msg[1], 'yellow' )
etype = 'error';
break;
case HMSTATUS.themeHelperLoad:
msg = printf(M2C( this.msgs.themeHelperLoad.msg ), ex.glob);
etype = 'error';
break;
case HMSTATUS.invalidSchemaVersion:
msg = printf(M2C( this.msgs.invalidSchemaVersion.msg ), ex.data);
etype = 'error';
break;
}
return {
msg, // The error message to display
withStack, // Whether to include the stack
quit,
etype
};
};

View File

@ -1,390 +0,0 @@
###*
Definition of the `main` function.
@module cli/main
@license MIT. See LICENSE.md for details.
###
HMR = require '../index'
PKG = require '../../package.json'
FS = require 'fs'
EXTEND = require 'extend'
chalk = require 'chalk'
PATH = require 'path'
HMSTATUS = require '../core/status-codes'
safeLoadJSON = require '../utils/safe-json-loader'
#StringUtils = require '../utils/string.js'
_ = require 'underscore'
OUTPUT = require './out'
PAD = require 'string-padding'
Command = require('commander').Command
M2C = require '../utils/md2chalk'
printf = require 'printf'
_opts = { }
_title = chalk.white.bold('\n*** HackMyResume v' +PKG.version+ ' ***')
_out = new OUTPUT( _opts )
_err = require('./error')
_exitCallback = null
###
A callable implementation of the HackMyResume CLI. Encapsulates the command
line interface as a single method accepting a parameter array.
@alias module:cli/main.main
@param rawArgs {Array} An array of command-line parameters. Will either be
process.argv (in production) or custom parameters (in test).
###
module.exports = ( rawArgs, exitCallback ) ->
initInfo = initialize( rawArgs, exitCallback )
if initInfo is null
return
args = initInfo.args
# Create the top-level (application) command...
program = new Command('hackmyresume')
.version(PKG.version)
.description(chalk.yellow.bold('*** HackMyResume ***'))
.option('-s --silent', 'Run in silent mode')
.option('--no-color', 'Disable colors')
.option('--color', 'Enable colors')
.option('-d --debug', 'Enable diagnostics', false)
.option('-a --assert', 'Treat warnings as errors', false)
.option('-v --version', 'Show the version')
.allowUnknownOption();
program.jsonArgs = initInfo.options
# Create the NEW command
program
.command 'new'
.arguments '<sources...>'
.option '-f --format <fmt>', 'FRESH or JRS format', 'FRESH'
.alias 'create'
.description 'Create resume(s) in FRESH or JSON RESUME format.'
.action (( sources ) ->
execute.call( this, sources, [], this.opts(), logMsg)
return
)
# Create the VALIDATE command
program
.command('validate')
.arguments('<sources...>')
.description('Validate a resume in FRESH or JSON RESUME format.')
.action((sources) ->
execute.call( this, sources, [], this.opts(), logMsg)
return
)
# Create the CONVERT command
program
.command('convert')
.description('Convert a resume to/from FRESH or JSON RESUME format.')
.option('-f --format <fmt>', 'FRESH or JRS format and optional version', undefined)
.action(->
x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg)
return
)
# Create the ANALYZE command
program
.command('analyze')
.arguments('<sources...>')
.option('--private', 'Include resume fields marked as private', false)
.description('Analyze one or more resumes.')
.action(( sources ) ->
execute.call( this, sources, [], this.opts(), logMsg)
return
)
# Create the PEEK command
program
.command('peek')
.arguments('<sources...>')
.description('Peek at a resume field or section')
#.action(( sources, sectionOrField ) ->
.action(( sources ) ->
dst = if (sources && sources.length > 1) then [sources.pop()] else []
execute.call( this, sources, dst, this.opts(), logMsg)
return
)
# Create the BUILD command
program
.command('build')
.alias('generate')
.option('-t --theme <theme>', 'Theme name or path')
.option('-n --no-prettify', 'Disable HTML prettification', true)
.option('-c --css <option>', 'CSS linking / embedding')
.option('-p --pdf <engine>', 'PDF generation engine')
.option('--no-sort', 'Sort resume sections by date', false)
.option('--tips', 'Display theme tips and warnings.', false)
.option('--private', 'Include resume fields marked as private', false)
.option('--no-escape', 'Turn off encoding in Handlebars themes.', false)
.description('Generate resume to multiple formats')
#.action(( sources, targets, options ) ->
.action(->
x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg)
return
)
# Create the HELP command
program
.command('help')
.arguments('[command]')
.description('Get help on a HackMyResume command')
.action ( cmd ) ->
cmd = cmd || 'use'
manPage = FS.readFileSync(
PATH.join(__dirname, 'help/' + cmd + '.txt'),
'utf8')
_out.log M2C(manPage, 'white', 'yellow.bold')
return
program.parse( args )
if !program.args.length
throw fluenterror: 4
### Massage command-line args and setup Commander.js. ###
initialize = ( ar, exitCallback ) ->
_exitCallback = exitCallback || process.exit
o = initOptions ar
if o.ex
_err.init false, true, false
if( o.ex.op == 'parse' )
_err.err
fluenterror: if o.ex.op == 'parse' then HMSTATUS.invalidOptionsFile else HMSTATUS.optionsFileNotFound,
inner: o.ex.inner,
quit: true
else
_err.err fluenterror: HMSTATUS.optionsFileNotFound, inner: o.ex.inner, quit: true
return null
o.silent || logMsg( _title )
# Emit debug prelude if --debug was specified
if o.debug
_out.log(chalk.cyan('The -d or --debug switch was specified. DEBUG mode engaged.'))
_out.log('')
_out.log(chalk.cyan(PAD(' Platform:',25, null, PAD.RIGHT)) + chalk.cyan.bold( if process.platform == 'win32' then 'windows' else process.platform ))
_out.log(chalk.cyan(PAD(' Node.js:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.version ))
_out.log(chalk.cyan(PAD(' HackMyResume:',25, null, PAD.RIGHT)) + chalk.cyan.bold('v' + PKG.version ))
_out.log(chalk.cyan(PAD(' FRESCA:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies.fresca ))
#_out.log(chalk.cyan(PAD(' fresh-themes:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-themes'] ))
#_out.log(chalk.cyan(PAD(' fresh-jrs-converter:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-jrs-converter'] ))
_out.log('')
_err.init o.debug, o.assert, o.silent
# Handle invalid verbs here (a bit easier here than in commander.js)...
if o.verb && !HMR.verbs[ o.verb ] && !HMR.alias[ o.verb ] && o.verb != 'help'
_err.err fluenterror: HMSTATUS.invalidCommand, quit: true, attempted: o.orgVerb, true
# Override the .missingArgument behavior
Command.prototype.missingArgument = (### unused ###) ->
if this.name() != 'help'
_err.err
verb: @name()
fluenterror: HMSTATUS.resumeNotFound
, true
return
# Override the .helpInformation behavior
Command.prototype.helpInformation = ->
manPage = FS.readFileSync(
PATH.join(__dirname, 'help/use.txt'), 'utf8' )
return M2C(manPage, 'white', 'yellow')
return {
args: o.args,
options: o.json
}
### Init options prior to setting up command infrastructure. ###
initOptions = ( ar ) ->
oVerb
verb = ''
args = ar.slice()
cleanArgs = args.slice( 2 )
oJSON
if cleanArgs.length
# Support case-insensitive sub-commands (build, generate, validate, etc)
vidx = _.findIndex cleanArgs, (v) -> v[0] != '-'
if vidx != -1
oVerb = cleanArgs[ vidx ]
verb = args[ vidx + 2 ] = oVerb.trim().toLowerCase()
# Remove --options --opts -o and process separately
optsIdx = _.findIndex cleanArgs, (v) ->
v == '-o' || v == '--options' || v == '--opts'
if optsIdx != -1
optStr = cleanArgs[ optsIdx + 1]
args.splice( optsIdx + 2, 2 )
if optStr && (optStr = optStr.trim())
#var myJSON = JSON.parse(optStr);
if( optStr[0] == '{')
# TODO: remove use of evil(). - hacksalot
### jshint ignore:start ###
oJSON = eval('(' + optStr + ')') # jshint ignore:line <-- no worky
### jshint ignore:end ###
else
inf = safeLoadJSON( optStr )
if( !inf.ex )
oJSON = inf.json
else
return inf
# Grab the --debug flag, --silent, --assert and --no-color flags
isDebug = _.some args, (v) -> v == '-d' || v == '--debug'
isSilent = _.some args, (v) -> v == '-s' || v == '--silent'
isAssert = _.some args, (v) -> v == '-a' || v == '--assert'
isMono = _.some args, (v) -> v == '--no-color'
isNoEscape = _.some args, (v) -> v == '--no-escape'
return {
color: !isMono,
debug: isDebug,
silent: isSilent,
assert: isAssert,
noescape: isNoEscape,
orgVerb: oVerb,
verb: verb,
json: oJSON,
args: args
}
### Invoke a HackMyResume verb. ###
execute = ( src, dst, opts, log ) ->
# Create the verb
v = new HMR.verbs[ @name() ]()
# Initialize command-specific options
loadOptions.call this, opts, this.parent.jsonArgs
# Set up error/output handling
_opts.errHandler = v
_out.init _opts
# Hook up event notifications
v.on 'hmr:status', -> _out.do.apply _out, arguments
v.on 'hmr:error', -> _err.err.apply _err, arguments
# Invoke the verb using promise syntax
prom = v.invoke.call v, src, dst, _opts, log
prom.then executeSuccess, executeFail
return
### Success handler for verb invocations. Calls process.exit by default ###
executeSuccess = (###obj###) ->
# Can't call _exitCallback here (process.exit) when PDF is running in BK
#_exitCallback 0; return
### Failure handler for verb invocations. Calls process.exit by default ###
executeFail = (err) ->
#console.dir err
finalErrorCode = -1
if err
if err.fluenterror
finalErrorCode = err.fluenterror
else if err.length
finalErrorCode = err[0].fluenterror
else
finalErrorCode = err
if _opts.debug
msgs = require('./msg').errors;
logMsg printf M2C( msgs.exiting.msg, 'cyan' ), finalErrorCode
logMsg err.stack if err.stack
_exitCallback finalErrorCode
return
###
Initialize HackMyResume options.
TODO: Options loading is a little hacky, for two reasons:
- Commander.js idiosyncracies
- Need to accept JSON inputs from the command line.
###
loadOptions = ( o, cmdO ) ->
# o and this.opts() seem to be the same (command-specific options)
# Load the specified options file (if any) and apply options
if( cmdO )
o = EXTEND(true, o, cmdO)
# Merge in command-line options
o = EXTEND( true, o, this.opts() )
# Kludge parent-level options until piping issue is resolved
if this.parent.silent != undefined && this.parent.silent != null
o.silent = this.parent.silent
if this.parent.debug != undefined && this.parent.debug != null
o.debug = this.parent.debug
if this.parent.assert != undefined && this.parent.assert != null
o.assert = this.parent.assert
if o.debug
logMsg(chalk.cyan('OPTIONS:') + '\n')
_.each(o, (val, key) ->
logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'),
PAD(key,22,null,PAD.RIGHT), val)
);
logMsg('');
# Cache
EXTEND( true, _opts, o )
return
### Split multiple command-line filenames by the 'TO' keyword ###
splitSrcDest = () ->
params = this.parent.args.filter((j) -> return String.is(j) )
if params.length == 0
#tmpName = @name()
throw { fluenterror: HMSTATUS.resumeNotFound, verb: @name(), quit: true }
# Find the TO keyword, if any
splitAt = _.findIndex( params, (p) -> return p.toLowerCase() == 'to'; )
# TO can't be the last keyword
if splitAt == params.length - 1 && splitAt != -1
logMsg(chalk.yellow('Please ') +
chalk.yellow.bold('specify an output file') +
chalk.yellow(' for this operation or ') +
chalk.yellow.bold('omit the TO keyword') +
chalk.yellow('.') )
return
return {
src: params.slice(0, if splitAt == -1 then undefined else splitAt ),
dst: if splitAt == -1 then [] else params.slice( splitAt + 1 )
}
### Simple logging placeholder. ###
logMsg = () ->
# eslint-disable-next-line no-console
_opts.silent || console.log.apply( console.log, arguments )

421
src/cli/main.js Normal file
View File

@ -0,0 +1,421 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the `main` function.
@module cli/main
@license MIT. See LICENSE.md for details.
*/
const HMR = require('../index');
const PKG = require('../../package.json');
const FS = require('fs');
const EXTEND = require('extend');
const chalk = require('chalk');
const PATH = require('path');
const HMSTATUS = require('../core/status-codes');
const safeLoadJSON = require('../utils/safe-json-loader');
//StringUtils = require '../utils/string.js'
const _ = require('underscore');
const OUTPUT = require('./out');
const PAD = require('string-padding');
const { Command } = require('commander');
const M2C = require('../utils/md2chalk');
const printf = require('printf');
const _opts = { };
const _title = chalk.white.bold(`\n*** HackMyResume v${PKG.version} ***`);
const _out = new OUTPUT( _opts );
const _err = require('./error');
let _exitCallback = null;
/*
A callable implementation of the HackMyResume CLI. Encapsulates the command
line interface as a single method accepting a parameter array.
@alias module:cli/main.main
@param rawArgs {Array} An array of command-line parameters. Will either be
process.argv (in production) or custom parameters (in test).
*/
module.exports = function( rawArgs, exitCallback ) {
const initInfo = initialize( rawArgs, exitCallback );
if (initInfo === null) {
return;
}
const { args } = initInfo;
// Create the top-level (application) command...
const program = new Command('hackmyresume')
.version(PKG.version)
.description(chalk.yellow.bold('*** HackMyResume ***'))
.option('-s --silent', 'Run in silent mode')
.option('--no-color', 'Disable colors')
.option('--color', 'Enable colors')
.option('-d --debug', 'Enable diagnostics', false)
.option('-a --assert', 'Treat warnings as errors', false)
.option('-v --version', 'Show the version')
.allowUnknownOption();
program.jsonArgs = initInfo.options;
// Create the NEW command
program
.command('new')
.arguments('<sources...>')
.option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
.alias('create')
.description('Create resume(s) in FRESH or JSON RESUME format.')
.action((function( sources ) {
execute.call( this, sources, [], this.opts(), logMsg);
})
);
// Create the VALIDATE command
program
.command('validate')
.arguments('<sources...>')
.description('Validate a resume in FRESH or JSON RESUME format.')
.action(function(sources) {
execute.call( this, sources, [], this.opts(), logMsg);
});
// Create the CONVERT command
program
.command('convert')
.description('Convert a resume to/from FRESH or JSON RESUME format.')
.option('-f --format <fmt>', 'FRESH or JRS format and optional version', undefined)
.action(function() {
const x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg);
});
// Create the ANALYZE command
program
.command('analyze')
.arguments('<sources...>')
.option('--private', 'Include resume fields marked as private', false)
.description('Analyze one or more resumes.')
.action(function( sources ) {
execute.call( this, sources, [], this.opts(), logMsg);
});
// Create the PEEK command
program
.command('peek')
.arguments('<sources...>')
.description('Peek at a resume field or section')
//.action(( sources, sectionOrField ) ->
.action(function( sources ) {
const dst = (sources && (sources.length > 1)) ? [sources.pop()] : [];
execute.call( this, sources, dst, this.opts(), logMsg);
});
// Create the BUILD command
program
.command('build')
.alias('generate')
.option('-t --theme <theme>', 'Theme name or path')
.option('-n --no-prettify', 'Disable HTML prettification', true)
.option('-c --css <option>', 'CSS linking / embedding')
.option('-p --pdf <engine>', 'PDF generation engine')
.option('--no-sort', 'Sort resume sections by date', false)
.option('--tips', 'Display theme tips and warnings.', false)
.option('--private', 'Include resume fields marked as private', false)
.option('--no-escape', 'Turn off encoding in Handlebars themes.', false)
.description('Generate resume to multiple formats')
//.action(( sources, targets, options ) ->
.action(function() {
const x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg);
});
// Create the HELP command
program
.command('help')
.arguments('[command]')
.description('Get help on a HackMyResume command')
.action(function( cmd ) {
cmd = cmd || 'use';
const manPage = FS.readFileSync(
PATH.join(__dirname, `help/${cmd}.txt`),
'utf8');
_out.log(M2C(manPage, 'white', 'yellow.bold'));
});
program.parse( args );
if (!program.args.length) {
throw {fluenterror: 4};
}
};
/* Massage command-line args and setup Commander.js. */
var initialize = function( ar, exitCallback ) {
_exitCallback = exitCallback || process.exit;
const o = initOptions(ar);
if (o.ex) {
_err.init(false, true, false);
if( o.ex.op === 'parse' ) {
_err.err({
fluenterror: o.ex.op === 'parse' ? HMSTATUS.invalidOptionsFile : HMSTATUS.optionsFileNotFound,
inner: o.ex.inner,
quit: true
});
} else {
_err.err({fluenterror: HMSTATUS.optionsFileNotFound, inner: o.ex.inner, quit: true});
}
return null;
}
o.silent || logMsg( _title );
// Emit debug prelude if --debug was specified
if (o.debug) {
_out.log(chalk.cyan('The -d or --debug switch was specified. DEBUG mode engaged.'));
_out.log('');
_out.log(chalk.cyan(PAD(' Platform:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.platform === 'win32' ? 'windows' : process.platform ));
_out.log(chalk.cyan(PAD(' Node.js:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.version ));
_out.log(chalk.cyan(PAD(' HackMyResume:',25, null, PAD.RIGHT)) + chalk.cyan.bold(`v${PKG.version}` ));
_out.log(chalk.cyan(PAD(' FRESCA:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies.fresca ));
//_out.log(chalk.cyan(PAD(' fresh-themes:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-themes'] ))
//_out.log(chalk.cyan(PAD(' fresh-jrs-converter:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-jrs-converter'] ))
_out.log('');
}
_err.init(o.debug, o.assert, o.silent);
// Handle invalid verbs here (a bit easier here than in commander.js)...
if (o.verb && !HMR.verbs[ o.verb ] && !HMR.alias[ o.verb ] && (o.verb !== 'help')) {
_err.err({fluenterror: HMSTATUS.invalidCommand, quit: true, attempted: o.orgVerb}, true);
}
// Override the .missingArgument behavior
Command.prototype.missingArgument = function() {
if (this.name() !== 'help') {
_err.err({
verb: this.name(),
fluenterror: HMSTATUS.resumeNotFound
}
, true);
}
};
// Override the .helpInformation behavior
Command.prototype.helpInformation = function() {
const manPage = FS.readFileSync(
PATH.join(__dirname, 'help/use.txt'), 'utf8' );
return M2C(manPage, 'white', 'yellow');
};
return {
args: o.args,
options: o.json
};
};
/* Init options prior to setting up command infrastructure. */
var initOptions = function( ar ) {
let oJSON, oVerb;
oVerb;
let verb = '';
const args = ar.slice();
const cleanArgs = args.slice( 2 );
oJSON;
if (cleanArgs.length) {
// Support case-insensitive sub-commands (build, generate, validate, etc)
const vidx = _.findIndex(cleanArgs, v => v[0] !== '-');
if (vidx !== -1) {
oVerb = cleanArgs[ vidx ];
verb = (args[ vidx + 2 ] = oVerb.trim().toLowerCase());
}
// Remove --options --opts -o and process separately
const optsIdx = _.findIndex(cleanArgs, v => (v === '-o') || (v === '--options') || (v === '--opts'));
if (optsIdx !== -1) {
let optStr = cleanArgs[ optsIdx + 1];
args.splice( optsIdx + 2, 2 );
if (optStr && (optStr = optStr.trim())) {
//var myJSON = JSON.parse(optStr);
if( optStr[0] === '{') {
// TODO: remove use of evil(). - hacksalot
/* jshint ignore:start */
oJSON = eval(`(${optStr})`); // jshint ignore:line <-- no worky
/* jshint ignore:end */
} else {
const inf = safeLoadJSON( optStr );
if( !inf.ex ) {
oJSON = inf.json;
} else {
return inf;
}
}
}
}
}
// Grab the --debug flag, --silent, --assert and --no-color flags
const isDebug = _.some(args, v => (v === '-d') || (v === '--debug'));
const isSilent = _.some(args, v => (v === '-s') || (v === '--silent'));
const isAssert = _.some(args, v => (v === '-a') || (v === '--assert'));
const isMono = _.some(args, v => v === '--no-color');
const isNoEscape = _.some(args, v => v === '--no-escape');
return {
color: !isMono,
debug: isDebug,
silent: isSilent,
assert: isAssert,
noescape: isNoEscape,
orgVerb: oVerb,
verb,
json: oJSON,
args
};
};
/* Invoke a HackMyResume verb. */
var execute = function( src, dst, opts, log ) {
// Create the verb
const v = new (HMR.verbs[ this.name() ])();
// Initialize command-specific options
loadOptions.call(this, opts, this.parent.jsonArgs);
// Set up error/output handling
_opts.errHandler = v;
_out.init(_opts);
// Hook up event notifications
v.on('hmr:status', function() { return _out.do.apply(_out, arguments); });
v.on('hmr:error', function() { return _err.err.apply(_err, arguments); });
// Invoke the verb using promise syntax
const prom = v.invoke.call(v, src, dst, _opts, log);
prom.then(executeSuccess, executeFail);
};
/* Success handler for verb invocations. Calls process.exit by default */
var executeSuccess = function() {};
// Can't call _exitCallback here (process.exit) when PDF is running in BK
//_exitCallback 0; return
/* Failure handler for verb invocations. Calls process.exit by default */
var executeFail = function(err) {
//console.dir err
let finalErrorCode = -1;
if (err) {
if (err.fluenterror) {
finalErrorCode = err.fluenterror;
} else if (err.length) {
finalErrorCode = err[0].fluenterror;
} else {
finalErrorCode = err;
}
}
if (_opts.debug) {
const msgs = require('./msg').errors;
logMsg(printf(M2C( msgs.exiting.msg, 'cyan' ), finalErrorCode));
if (err.stack) { logMsg(err.stack); }
}
_exitCallback(finalErrorCode);
};
/*
Initialize HackMyResume options.
TODO: Options loading is a little hacky, for two reasons:
- Commander.js idiosyncracies
- Need to accept JSON inputs from the command line.
*/
var loadOptions = function( o, cmdO ) {
// o and this.opts() seem to be the same (command-specific options)
// Load the specified options file (if any) and apply options
if( cmdO ) {
o = EXTEND(true, o, cmdO);
}
// Merge in command-line options
o = EXTEND( true, o, this.opts() );
// Kludge parent-level options until piping issue is resolved
if ((this.parent.silent !== undefined) && (this.parent.silent !== null)) {
o.silent = this.parent.silent;
}
if ((this.parent.debug !== undefined) && (this.parent.debug !== null)) {
o.debug = this.parent.debug;
}
if ((this.parent.assert !== undefined) && (this.parent.assert !== null)) {
o.assert = this.parent.assert;
}
if (o.debug) {
logMsg(chalk.cyan('OPTIONS:') + '\n');
_.each(o, (val, key) =>
logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'),
PAD(key,22,null,PAD.RIGHT), val)
);
logMsg('');
}
// Cache
EXTEND( true, _opts, o );
};
/* Split multiple command-line filenames by the 'TO' keyword */
var splitSrcDest = function() {
const params = this.parent.args.filter(j => String.is(j));
if (params.length === 0) {
//tmpName = @name()
throw { fluenterror: HMSTATUS.resumeNotFound, verb: this.name(), quit: true };
}
// Find the TO keyword, if any
const splitAt = _.findIndex( params, p => p.toLowerCase() === 'to');
// TO can't be the last keyword
if ((splitAt === (params.length - 1)) && (splitAt !== -1)) {
logMsg(chalk.yellow('Please ') +
chalk.yellow.bold('specify an output file') +
chalk.yellow(' for this operation or ') +
chalk.yellow.bold('omit the TO keyword') +
chalk.yellow('.') );
return;
}
return {
src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
};
};
/* Simple logging placeholder. */
var logMsg = function() {
// eslint-disable-next-line no-console
return _opts.silent || console.log.apply( console.log, arguments );
};

View File

@ -1,10 +0,0 @@
###*
Message-handling routines for HackMyResume.
@module cli/msg
@license MIT. See LICENSE.md for details.
###
PATH = require 'path'
YAML = require 'yamljs'
module.exports = YAML.load PATH.join __dirname, 'msg.yml'

10
src/cli/msg.js Normal file
View File

@ -0,0 +1,10 @@
/**
Message-handling routines for HackMyResume.
@module cli/msg
@license MIT. See LICENSE.md for details.
*/
const PATH = require('path');
const YAML = require('yamljs');
module.exports = YAML.load(PATH.join(__dirname, 'msg.yml'));

View File

@ -1,182 +0,0 @@
###*
Output routines for HackMyResume.
@license MIT. See LICENSE.md for details.
@module cli/out
###
chalk = require('chalk')
HME = require('../core/event-codes')
_ = require('underscore')
M2C = require('../utils/md2chalk.js')
PATH = require('path')
FS = require('fs')
EXTEND = require('extend')
HANDLEBARS = require('handlebars')
YAML = require('yamljs')
printf = require('printf')
pad = require('string-padding')
dbgStyle = 'cyan';
###* A stateful output module. All HMR console output handled here. ###
class OutputHandler
constructor: ( opts ) ->
@init opts
return
init: (opts) ->
@opts = EXTEND( true, @opts || { }, opts )
@msgs = YAML.load(PATH.join( __dirname, 'msg.yml' )).events
return
log: ->
printf = require('printf')
finished = printf.apply( printf, arguments )
@opts.silent || console.log( finished ) # eslint-disable-line no-console
do: ( evt ) ->
that = @
L = () -> that.log.apply( that, arguments )
switch evt.sub
when HME.begin
this.opts.debug &&
L( M2C( this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase() )
#when HME.beforeCreate
#L( M2C( this.msgs.beforeCreate.msg, 'green' ), evt.fmt, evt.file )
#break;
when HME.afterCreate
L( M2C( @msgs.beforeCreate.msg, if evt.isError then 'red' else 'green' ), evt.fmt, evt.file )
break;
when HME.beforeTheme
this.opts.debug &&
L( M2C( this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase() )
when HME.afterParse
L( M2C( this.msgs.afterRead.msg, 'gray', 'white.dim'), evt.fmt.toUpperCase(), evt.file )
when HME.beforeMerge
msg = ''
evt.f.reverse().forEach ( a, idx ) ->
msg += printf( (if idx == 0 then @msgs.beforeMerge.msg[0] else @msgs.beforeMerge.msg[1]), a.file )
, @
L( M2C(msg, (if evt.mixed then 'yellow' else 'gray'), 'white.dim') )
when HME.applyTheme
@theme = evt.theme;
numFormats = Object.keys( evt.theme.formats ).length;
L( M2C(this.msgs.applyTheme.msg,
if evt.status == 'error' then 'red' else 'gray',
if evt.status == 'error' then 'bold' else 'white.dim'),
evt.theme.name.toUpperCase(),
numFormats, if numFormats == 1 then '' else 's' )
when HME.end
if evt.cmd == 'build'
themeName = this.theme.name.toUpperCase()
if this.opts.tips && (this.theme.message || this.theme.render)
if this.theme.message
L( M2C( this.msgs.afterBuild.msg[0], 'cyan' ), themeName )
L( M2C( this.theme.message, 'white' ))
else if this.theme.render
L( M2C( this.msgs.afterBuild.msg[0], 'cyan'), themeName)
L( M2C( this.msgs.afterBuild.msg[1], 'white'))
when HME.afterGenerate
suffix = ''
if evt.fmt == 'pdf'
if this.opts.pdf
if this.opts.pdf != 'none'
suffix = printf( M2C( this.msgs.afterGenerate.msg[0], if evt.error then 'red' else 'green' ), this.opts.pdf )
else
L( M2C( this.msgs.afterGenerate.msg[1], 'gray' ), evt.fmt.toUpperCase(), evt.file )
return
L( M2C( this.msgs.afterGenerate.msg[2] + suffix, if evt.error then 'red' else 'green' ),
pad( evt.fmt.toUpperCase(),4,null,pad.RIGHT ),
PATH.relative( process.cwd(), evt.file ) );
when HME.beforeAnalyze
L( M2C( this.msgs.beforeAnalyze.msg, 'green' ), evt.fmt, evt.file)
when HME.afterAnalyze
info = evt.info
rawTpl = FS.readFileSync( PATH.join( __dirname, 'analyze.hbs' ), 'utf8')
HANDLEBARS.registerHelper( require('../helpers/console-helpers') )
template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false })
tot = 0
info.keywords.forEach (g) -> tot += g.count
info.keywords.totalKeywords = tot
output = template( info )
@log( chalk.cyan(output) )
when HME.beforeConvert
L( M2C( this.msgs.beforeConvert.msg, if evt.error then 'red' else 'green' ),
evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt
);
when HME.afterInlineConvert
L( M2C( this.msgs.afterInlineConvert.msg, 'gray', 'white.dim' ),
evt.file, evt.fmt );
when HME.afterValidate
style = 'red'
adj = ''
msgs = @msgs.afterValidate.msg;
switch evt.status
when 'valid' then style = 'green'; adj = msgs[1]
when 'invalid' then style = 'yellow'; adj = msgs[2]
when 'broken' then style = 'red'; adj = msgs[3]
when 'missing' then style = 'red'; adj = msgs[4]
when 'unknown' then style = 'red'; adj = msgs[5]
evt.schema = evt.schema.replace('jars','JSON Resume').toUpperCase()
L(M2C( msgs[0], 'white' ) + chalk[style].bold(adj), evt.file, evt.schema)
if evt.violations
_.each evt.violations, (err) ->
L( chalk.yellow.bold('--> ') +
chalk.yellow(err.field.replace('data.','resume.').toUpperCase() +
' ' + err.message))
return
, @
return
when HME.afterPeek
sty = if evt.error then 'red' else ( if evt.target != undefined then 'green' else 'yellow' )
# "Peeking at 'someKey' in 'someFile'."
if evt.requested
L(M2C(this.msgs.beforePeek.msg[0], sty), evt.requested, evt.file)
else
L(M2C(this.msgs.beforePeek.msg[1], sty), evt.file)
# If the key was present, print it
if evt.target != undefined and !evt.error
# eslint-disable-next-line no-console
console.dir( evt.target, { depth: null, colors: true } )
# If the key was not present, but no error occurred, print it
else if !evt.error
L M2C( this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file
else if evt.error
L chalk.red( evt.error.inner.inner )
module.exports = OutputHandler

204
src/cli/out.js Normal file
View File

@ -0,0 +1,204 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Output routines for HackMyResume.
@license MIT. See LICENSE.md for details.
@module cli/out
*/
const chalk = require('chalk');
const HME = require('../core/event-codes');
const _ = require('underscore');
const M2C = require('../utils/md2chalk.js');
const PATH = require('path');
const FS = require('fs');
const EXTEND = require('extend');
const HANDLEBARS = require('handlebars');
const YAML = require('yamljs');
let printf = require('printf');
const pad = require('string-padding');
const dbgStyle = 'cyan';
/** A stateful output module. All HMR console output handled here. */
class OutputHandler {
constructor( opts ) {
this.init(opts);
}
init(opts) {
this.opts = EXTEND( true, this.opts || { }, opts );
this.msgs = YAML.load(PATH.join( __dirname, 'msg.yml' )).events;
}
log() {
printf = require('printf');
const finished = printf.apply( printf, arguments );
return this.opts.silent || console.log( finished ); // eslint-disable-line no-console
}
do( evt ) {
const that = this;
const L = function() { return that.log.apply( that, arguments ); };
switch (evt.sub) {
case HME.begin:
return this.opts.debug &&
L( M2C( this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase() );
//when HME.beforeCreate
//L( M2C( this.msgs.beforeCreate.msg, 'green' ), evt.fmt, evt.file )
//break;
case HME.afterCreate:
L( M2C( this.msgs.beforeCreate.msg, evt.isError ? 'red' : 'green' ), evt.fmt, evt.file );
break;
case HME.beforeTheme:
return this.opts.debug &&
L( M2C( this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase() );
case HME.afterParse:
return L( M2C( this.msgs.afterRead.msg, 'gray', 'white.dim'), evt.fmt.toUpperCase(), evt.file );
case HME.beforeMerge:
var msg = '';
evt.f.reverse().forEach(function( a, idx ) {
return msg += printf( (idx === 0 ? this.msgs.beforeMerge.msg[0] : this.msgs.beforeMerge.msg[1]), a.file );
}
, this);
return L( M2C(msg, (evt.mixed ? 'yellow' : 'gray'), 'white.dim') );
case HME.applyTheme:
this.theme = evt.theme;
var numFormats = Object.keys( evt.theme.formats ).length;
return L( M2C(this.msgs.applyTheme.msg,
evt.status === 'error' ? 'red' : 'gray',
evt.status === 'error' ? 'bold' : 'white.dim'),
evt.theme.name.toUpperCase(),
numFormats, numFormats === 1 ? '' : 's' );
case HME.end:
if (evt.cmd === 'build') {
const themeName = this.theme.name.toUpperCase();
if (this.opts.tips && (this.theme.message || this.theme.render)) {
if (this.theme.message) {
L( M2C( this.msgs.afterBuild.msg[0], 'cyan' ), themeName );
return L( M2C( this.theme.message, 'white' ));
} else if (this.theme.render) {
L( M2C( this.msgs.afterBuild.msg[0], 'cyan'), themeName);
return L( M2C( this.msgs.afterBuild.msg[1], 'white'));
}
}
}
break;
case HME.afterGenerate:
var suffix = '';
if (evt.fmt === 'pdf') {
if (this.opts.pdf) {
if (this.opts.pdf !== 'none') {
suffix = printf( M2C( this.msgs.afterGenerate.msg[0], evt.error ? 'red' : 'green' ), this.opts.pdf );
} else {
L( M2C( this.msgs.afterGenerate.msg[1], 'gray' ), evt.fmt.toUpperCase(), evt.file );
return;
}
}
}
return L( M2C( this.msgs.afterGenerate.msg[2] + suffix, evt.error ? 'red' : 'green' ),
pad( evt.fmt.toUpperCase(),4,null,pad.RIGHT ),
PATH.relative( process.cwd(), evt.file ) );
case HME.beforeAnalyze:
return L( M2C( this.msgs.beforeAnalyze.msg, 'green' ), evt.fmt, evt.file);
case HME.afterAnalyze:
var { info } = evt;
var rawTpl = FS.readFileSync( PATH.join( __dirname, 'analyze.hbs' ), 'utf8');
HANDLEBARS.registerHelper( require('../helpers/console-helpers') );
var template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false });
var tot = 0;
info.keywords.forEach(g => tot += g.count);
info.keywords.totalKeywords = tot;
var output = template( info );
return this.log( chalk.cyan(output) );
case HME.beforeConvert:
return L( M2C( this.msgs.beforeConvert.msg, evt.error ? 'red' : 'green' ),
evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt
);
case HME.afterInlineConvert:
return L( M2C( this.msgs.afterInlineConvert.msg, 'gray', 'white.dim' ),
evt.file, evt.fmt );
case HME.afterValidate:
var style = 'red';
var adj = '';
var msgs = this.msgs.afterValidate.msg;
switch (evt.status) {
case 'valid': style = 'green'; adj = msgs[1]; break;
case 'invalid': style = 'yellow'; adj = msgs[2]; break;
case 'broken': style = 'red'; adj = msgs[3]; break;
case 'missing': style = 'red'; adj = msgs[4]; break;
case 'unknown': style = 'red'; adj = msgs[5]; break;
}
evt.schema = evt.schema.replace('jars','JSON Resume').toUpperCase();
L(M2C( msgs[0], 'white' ) + chalk[style].bold(adj), evt.file, evt.schema);
if (evt.violations) {
_.each(evt.violations, function(err) {
L( chalk.yellow.bold('--> ') +
chalk.yellow(err.field.replace('data.','resume.').toUpperCase() +
' ' + err.message));
}
, this);
}
return;
case HME.afterPeek:
var sty = evt.error ? 'red' : ( evt.target !== undefined ? 'green' : 'yellow' );
// "Peeking at 'someKey' in 'someFile'."
if (evt.requested) {
L(M2C(this.msgs.beforePeek.msg[0], sty), evt.requested, evt.file);
} else {
L(M2C(this.msgs.beforePeek.msg[1], sty), evt.file);
}
// If the key was present, print it
if ((evt.target !== undefined) && !evt.error) {
// eslint-disable-next-line no-console
return console.dir( evt.target, { depth: null, colors: true } );
// If the key was not present, but no error occurred, print it
} else if (!evt.error) {
return L(M2C( this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file);
} else if (evt.error) {
return L(chalk.red( evt.error.inner.inner ));
}
break;
}
}
}
module.exports = OutputHandler;

View File

@ -1,10 +1,10 @@
###
/*
Event code definitions.
@module core/default-formats
@license MIT. See LICENSE.md for details.
###
*/
###* Supported resume formats. ###
/** Supported resume formats. */
module.exports = [
{ name: 'html', ext: 'html', gen: new (require('../generators/html-generator'))() },
{ name: 'txt', ext: 'txt', gen: new (require('../generators/text-generator'))() },
@ -15,4 +15,4 @@ module.exports = [
{ name: 'json', ext: 'json', gen: new (require('../generators/json-generator'))() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../generators/json-yaml-generator'))() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../generators/latex-generator'))() }
]
];

View File

@ -1,13 +0,0 @@
###
Event code definitions.
@module core/default-options
@license MIT. See LICENSE.md for details.
###
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

View File

@ -0,0 +1,15 @@
/*
Event code definitions.
@module core/default-options
@license MIT. See LICENSE.md for details.
*/
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

View File

@ -1,38 +0,0 @@
###
Event code definitions.
@module core/event-codes
@license MIT. See LICENSE.md for details.
###
module.exports =
error: -1
success: 0
begin: 1
end: 2
beforeRead: 3
afterRead: 4
beforeCreate: 5
afterCreate: 6
beforeTheme: 7
afterTheme: 8
beforeMerge: 9
afterMerge: 10
beforeGenerate: 11
afterGenerate: 12
beforeAnalyze: 13
afterAnalyze: 14
beforeConvert: 15
afterConvert: 16
verifyOutputs: 17
beforeParse: 18
afterParse: 19
beforePeek: 20
afterPeek: 21
beforeInlineConvert: 22
afterInlineConvert: 23
beforeValidate: 24
afterValidate: 25
beforeWrite: 26
afterWrite: 27
applyTheme: 28

39
src/core/event-codes.js Normal file
View File

@ -0,0 +1,39 @@
/*
Event code definitions.
@module core/event-codes
@license MIT. See LICENSE.md for details.
*/
module.exports = {
error: -1,
success: 0,
begin: 1,
end: 2,
beforeRead: 3,
afterRead: 4,
beforeCreate: 5,
afterCreate: 6,
beforeTheme: 7,
afterTheme: 8,
beforeMerge: 9,
afterMerge: 10,
beforeGenerate: 11,
afterGenerate: 12,
beforeAnalyze: 13,
afterAnalyze: 14,
beforeConvert: 15,
afterConvert: 16,
verifyOutputs: 17,
beforeParse: 18,
afterParse: 19,
beforePeek: 20,
afterPeek: 21,
beforeInlineConvert: 22,
afterInlineConvert: 23,
beforeValidate: 24,
afterValidate: 25,
beforeWrite: 26,
afterWrite: 27,
applyTheme: 28
};

View File

@ -1,77 +0,0 @@
###*
The HackMyResume date representation.
@license MIT. See LICENSE.md for details.
@module core/fluent-date
###
moment = require 'moment'
require('../utils/string')
###*
Create a FluentDate from a string or Moment date object. There are a few date
formats to be aware of here.
1. The words "Present" and "Now", referring to the current date
2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10")
3. Year-and-month only ("2015-04")
4. Year-only "YYYY" ("2015")
5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008")
6. Empty dates ("", " ")
7. Any other date format that Moment.js can parse from
Note: Moment can transparently parse all or most of these, without requiring us
to specify a date format...but for maximum parsing safety and to avoid Moment
deprecation warnings, it's recommended to either a) explicitly specify the date
format or b) use an ISO format. For clarity, we handle these cases explicitly.
@class FluentDate
###
class FluentDate
constructor: (dt) ->
@rep = this.fmt dt
@isCurrent: (dt) ->
!dt || (String.is(dt) and /^(present|now|current)$/.test(dt))
months = {}
abbr = {}
moment.months().forEach((m,idx) -> months[m.toLowerCase()] = idx+1 )
moment.monthsShort().forEach((m,idx) -> abbr[m.toLowerCase()]=idx+1 )
abbr.sept = 9
module.exports = FluentDate
FluentDate.fmt = ( dt, throws ) ->
throws = (throws == undefined || throws == null) || throws
if typeof dt == 'string' or dt instanceof String
dt = dt.toLowerCase().trim()
if /^(present|now|current)$/.test(dt) # "Present", "Now"
return moment()
else if /^\D+\s+\d{4}$/.test(dt) # "Mar 2015"
parts = dt.split(' ');
month = (months[parts[0]] || abbr[parts[0]]);
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'
else if /^\s*\d{4}\s*$/.test(dt) # "2015"
return moment dt, 'YYYY'
else if /^\s*$/.test(dt) # "", " "
return moment()
else
mt = moment dt
if mt.isValid()
return mt
if throws
throw 'Invalid date format encountered.'
return null
else
if !dt
return moment()
else if dt.isValid and dt.isValid()
return dt
if throws
throw 'Unknown date object encountered.'
return null

95
src/core/fluent-date.js Normal file
View File

@ -0,0 +1,95 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
The HackMyResume date representation.
@license MIT. See LICENSE.md for details.
@module core/fluent-date
*/
const moment = require('moment');
require('../utils/string');
/**
Create a FluentDate from a string or Moment date object. There are a few date
formats to be aware of here.
1. The words "Present" and "Now", referring to the current date
2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10")
3. Year-and-month only ("2015-04")
4. Year-only "YYYY" ("2015")
5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008")
6. Empty dates ("", " ")
7. Any other date format that Moment.js can parse from
Note: Moment can transparently parse all or most of these, without requiring us
to specify a date format...but for maximum parsing safety and to avoid Moment
deprecation warnings, it's recommended to either a) explicitly specify the date
format or b) use an ISO format. For clarity, we handle these cases explicitly.
@class FluentDate
*/
class FluentDate {
constructor(dt) {
this.rep = this.fmt(dt);
}
static isCurrent(dt) {
return !dt || (String.is(dt) && /^(present|now|current)$/.test(dt));
}
}
const months = {};
const abbr = {};
moment.months().forEach((m,idx) => months[m.toLowerCase()] = idx+1);
moment.monthsShort().forEach((m,idx) => abbr[m.toLowerCase()]=idx+1);
abbr.sept = 9;
module.exports = FluentDate;
FluentDate.fmt = function( dt, throws ) {
throws = ((throws === undefined) || (throws === null)) || throws;
if ((typeof dt === 'string') || dt instanceof String) {
dt = dt.toLowerCase().trim();
if (/^(present|now|current)$/.test(dt)) { // "Present", "Now"
return moment();
} else if (/^\D+\s+\d{4}$/.test(dt)) { // "Mar 2015"
let left;
const parts = dt.split(' ');
const month = (months[parts[0]] || abbr[parts[0]]);
const temp = parts[1] + '-' + ((left = month < 10) != null ? left : `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');
} else if (/^\s*\d{4}\s*$/.test(dt)) { // "2015"
return moment(dt, 'YYYY');
} else if (/^\s*$/.test(dt)) { // "", " "
return moment();
} else {
const mt = moment(dt);
if (mt.isValid()) {
return mt;
}
if (throws) {
throw 'Invalid date format encountered.';
}
return null;
}
} else {
if (!dt) {
return moment();
} else if (dt.isValid && dt.isValid()) {
return dt;
}
if (throws) {
throw 'Unknown date object encountered.';
}
return null;
}
};

View File

@ -1,438 +0,0 @@
###*
Definition of the FRESHResume class.
@license MIT. See LICENSE.md for details.
@module core/fresh-resume
###
FS = require 'fs'
extend = require 'extend'
validator = require 'is-my-json-valid'
_ = require 'underscore'
__ = require 'lodash'
PATH = require 'path'
moment = require 'moment'
XML = require 'xml-escape'
MD = require 'marked'
CONVERTER = require 'fresh-jrs-converter'
JRSResume = require './jrs-resume'
FluentDate = require './fluent-date'
###*
A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
object is an instantiation of that JSON decorated with utility methods.
@constructor
###
class FreshResume# extends AbstractResume
###* Initialize the the FreshResume from JSON string data. ###
parse: ( stringData, opts ) ->
@imp = @imp ? raw: stringData
this.parseJSON JSON.parse( stringData ), opts
###*
Initialize the FreshResume from JSON.
Open and parse the specified FRESH resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
###
parseJSON: ( rep, opts ) ->
if opts and opts.privatize
# Ignore any element with the 'ignore: true' or 'private: true' designator.
scrubber = require '../utils/resume-scrubber'
{ scrubbed, ignoreList, privateList } = scrubber.scrubResume rep, opts
# Now apply the resume representation onto this object
extend true, @, if opts and opts.privatize then scrubbed else rep
# If the resume has already been processed, then we are being called from
# the .dupe method, and there's no need to do any post processing
if !@imp?.processed
# Set up metadata TODO: Clean up metadata on the object model.
opts = opts || { }
if opts.imp == undefined || opts.imp
@imp = @imp || { }
@imp.title = (opts.title || @imp.title) || @name
unless @imp.raw
@imp.raw = JSON.stringify rep
@imp.processed = true
# Parse dates, sort dates, and calculate computed values
(opts.date == undefined || opts.date) && _parseDates.call( this );
(opts.sort == undefined || opts.sort) && this.sort();
(opts.compute == undefined || opts.compute) && (@computed = {
numYears: this.duration(),
keywords: this.keywords()
});
@
###* Save the sheet to disk (for environments that have disk access). ###
save: ( filename ) ->
@imp.file = filename || @imp.file
FS.writeFileSync @imp.file, @stringify(), 'utf8'
@
###*
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
###
saveAs: ( filename, format ) ->
# If format isn't specified, default to FRESH
safeFormat = (format && format.trim()) || 'FRESH'
# Validate against the FRESH version regex
# freshVersionReg = require '../utils/fresh-version-regex'
# if (not freshVersionReg().test( safeFormat ))
# throw badVer: safeFormat
parts = safeFormat.split '@'
if parts[0] == 'FRESH'
@imp.file = filename || @imp.file
FS.writeFileSync @imp.file, @stringify(), 'utf8'
else if parts[0] == 'JRS'
useEdgeSchema = if parts.length > 1 then parts[1] == '1' else false
newRep = CONVERTER.toJRS @, edge: useEdgeSchema
FS.writeFileSync filename, JRSResume.stringify( newRep ), 'utf8'
else
throw badVer: safeFormat
@
###*
Duplicate this FreshResume instance.
This method first extend()s this object onto an empty, creating a deep copy,
and then passes the result into a new FreshResume instance via .parseJSON.
We do it this way to create a true clone of the object without re-running any
of the associated processing.
###
dupe: () ->
jso = extend true, { }, @
rnew = new FreshResume()
rnew.parseJSON jso, { }
rnew
###*
Convert this object to a JSON string, sanitizing meta-properties along the
way.
###
stringify: () -> FreshResume.stringify @
###*
Create a copy of this resume in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder).
TODO: Move this out of FRESHResume.
###
transformStrings: ( filt, transformer ) ->
ret = this.dupe()
trx = require '../utils/string-transformer'
trx ret, filt, transformer
###*
Create a copy of this resume in which all fields have been interpreted as
Markdown.
###
markdownify: () ->
MDIN = ( txt ) ->
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '')
trx = ( key, val ) ->
if key == 'summary'
return MD val
MDIN(val)
return @transformStrings ['skills','url','start','end','date'], trx
###*
Create a copy of this resume in which all fields have been interpreted as
Markdown.
###
xmlify: () ->
trx = (key, val) -> XML val
return @transformStrings [], trx
###* Return the resume format. ###
format: () -> 'FRESH'
###*
Return internal metadata. Create if it doesn't exist.
###
i: () -> this.imp = this.imp || { }
###*
Return a unique list of all skills declared in the resume.
###
# TODO: Several problems here:
# 1) Confusing name. Easily confused with the keyword-inspector module, which
# parses resume body text looking for these same keywords. This should probably
# be renamed.
#
# 2) Doesn't bother trying to integrate skills.list with skills.sets if they
# happen to declare different skills, and if skills.sets declares ONE skill and
# skills.list declared 50, only 1 skill will be registered.
#
# 3) In the future, skill.sets should only be able to use skills declared in
# skills.list. That is, skills.list is the official record of a candidate's
# declared skills. skills.sets is just a way of grouping those into skillsets
# for easier consumption.
keywords: () ->
flatSkills = []
if @skills
if @skills.sets
flatSkills = @skills.sets.map((sk) -> sk.skills ).reduce( (a,b) -> a.concat(b) )
else if @skills.list
flatSkills = flatSkills.concat( this.skills.list.map (sk) -> return sk.name )
flatSkills = _.uniq flatSkills
flatSkills
###*
Reset the sheet to an empty state. TODO: refactor/review
###
clear: ( clearMeta ) ->
clearMeta = ((clearMeta == undefined) && true) || clearMeta
delete this.imp if clearMeta
delete this.computed # Don't use Object.keys() here
delete this.employment
delete this.service
delete this.education
delete this.recognition
delete this.reading
delete this.writing
delete this.interests
delete this.skills
delete this.social
###*
Get a safe count of the number of things in a section.
###
count: ( obj ) ->
return 0 if !obj
return obj.history.length if obj.history
return obj.sets.length if obj.sets
obj.length || 0;
###* Add work experience to the sheet. ###
add: ( moniker ) ->
defSheet = FreshResume.default()
newObject =
if defSheet[moniker].history
then $.extend( true, {}, defSheet[ moniker ].history[0] )
else
if moniker == 'skills'
then $.extend( true, {}, defSheet.skills.sets[0] )
else $.extend( true, {}, defSheet[ moniker ][0] )
@[ moniker ] = @[ moniker ] || []
if @[ moniker ].history
@[ moniker ].history.push newObject
else if moniker == 'skills'
@skills.sets.push newObject
else
@[ moniker ].push newObject
newObject
###*
Determine if the sheet includes a specific social profile (eg, GitHub).
###
hasProfile: ( socialNetwork ) ->
socialNetwork = socialNetwork.trim().toLowerCase()
@social && _.some @social, (p) ->
p.network.trim().toLowerCase() == socialNetwork
###* Return the specified network profile. ###
getProfile: ( socialNetwork ) ->
socialNetwork = socialNetwork.trim().toLowerCase()
@social && _.find @social, (sn) ->
sn.network.trim().toLowerCase() == socialNetwork
###*
Return an array of profiles for the specified network, for when the user
has multiple eg. GitHub accounts.
###
getProfiles: ( socialNetwork ) ->
socialNetwork = socialNetwork.trim().toLowerCase()
@social && _.filter @social, (sn) ->
sn.network.trim().toLowerCase() == socialNetwork
###* Determine if the sheet includes a specific skill. ###
hasSkill: ( skill ) ->
skill = skill.trim().toLowerCase()
@skills && _.some @skills, (sk) ->
sk.keywords && _.some sk.keywords, (kw) ->
kw.trim().toLowerCase() == skill
###* Validate the sheet against the FRESH Resume schema. ###
isValid: ( info ) ->
schemaObj = require 'fresh-resume-schema'
validator = require 'is-my-json-valid'
validate = validator( schemaObj, { # See Note [1].
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
})
ret = validate @
if !ret
this.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
ret
duration: (unit) ->
inspector = require '../inspectors/duration-inspector'
inspector.run @, 'employment.history', 'start', 'end', unit
###*
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
###
sort: () ->
byDateDesc = (a,b) ->
if a.safe.start.isBefore(b.safe.start)
then 1
else ( if a.safe.start.isAfter(b.safe.start) then -1 else 0 )
sortSection = ( key ) ->
ar = __.get this, key
if ar && ar.length
datedThings = obj.filter (o) -> o.start
datedThings.sort( byDateDesc );
sortSection 'employment.history'
sortSection 'education.history'
sortSection 'service.history'
sortSection 'projects'
@writing && @writing.sort (a, b) ->
if a.safe.date.isBefore b.safe.date
then 1
else ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0
###*
Get the default (starter) sheet.
###
FreshResume.default = () ->
new FreshResume().parseJSON require('fresh-resume-starter').fresh
###*
Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
along the way.
###
FreshResume.stringify = ( obj ) ->
replacer = ( key,value ) -> # Exclude these keys from stringification
exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar']
return if _.some( exKeys, (val) -> key.trim() == val )
then undefined else value
JSON.stringify obj, replacer, 2
###*
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
###
_parseDates = () ->
_fmt = require('./fluent-date').fmt
that = @
# TODO: refactor recursion
replaceDatesInObject = ( obj ) ->
return if !obj
if Object.prototype.toString.call( obj ) == '[object Array]'
obj.forEach (elem) -> replaceDatesInObject( elem )
return
else if typeof obj == 'object'
if obj._isAMomentObject || obj.safe
return
Object.keys( obj ).forEach (key) -> replaceDatesInObject obj[key]
['start','end','date'].forEach (val) ->
if (obj[val] != undefined) && (!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'
return
return
Object.keys( this ).forEach (member) ->
replaceDatesInObject(that[member])
return
return
###* Export the Sheet function/ctor. ###
module.exports = FreshResume
# Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
# in addition to YYYY-MM-DD. The original regex:
#
# /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/
#

481
src/core/fresh-resume.js Normal file
View File

@ -0,0 +1,481 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the FRESHResume class.
@license MIT. See LICENSE.md for details.
@module core/fresh-resume
*/
const FS = require('fs');
const extend = require('extend');
let validator = require('is-my-json-valid');
const _ = require('underscore');
const __ = require('lodash');
const XML = require('xml-escape');
const MD = require('marked');
const CONVERTER = require('fresh-jrs-converter');
const JRSResume = require('./jrs-resume');
/**
A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
object is an instantiation of that JSON decorated with utility methods.
@constructor
*/
class FreshResume {// extends AbstractResume
/** Initialize the the FreshResume from JSON string data. */
parse( stringData, opts ) {
this.imp = this.imp != null ? this.imp : {raw: stringData};
return this.parseJSON(JSON.parse( stringData ), opts);
}
/**
Initialize the FreshResume from JSON.
Open and parse the specified FRESH resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
*/
parseJSON( rep, opts ) {
let scrubbed;
if (opts && opts.privatize) {
// Ignore any element with the 'ignore: true' or 'private: true' designator.
const scrubber = require('../utils/resume-scrubber');
var ret = scrubber.scrubResume(rep, opts);
scrubbed = ret.scrubbed;
}
// Now apply the resume representation onto this object
extend(true, this, opts && opts.privatize ? scrubbed : rep);
// If the resume has already been processed, then we are being called from
// the .dupe method, and there's no need to do any post processing
if (!(this.imp != null ? this.imp.processed : undefined)) {
// Set up metadata TODO: Clean up metadata on the object model.
opts = opts || { };
if ((opts.imp === undefined) || opts.imp) {
this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
}
}
this.imp.processed = true;
// Parse dates, sort dates, and calculate computed values
((opts.date === undefined) || opts.date) && _parseDates.call( this );
((opts.sort === undefined) || opts.sort) && this.sort();
((opts.compute === undefined) || opts.compute) && (this.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
}
return this;
}
/** Save the sheet to disk (for environments that have disk access). */
save( filename ) {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
return this;
}
/**
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
saveAs( filename, format ) {
// If format isn't specified, default to FRESH
const safeFormat = (format && format.trim()) || 'FRESH';
// Validate against the FRESH version regex
// freshVersionReg = require '../utils/fresh-version-regex'
// if (not freshVersionReg().test( safeFormat ))
// throw badVer: safeFormat
const parts = safeFormat.split('@');
if (parts[0] === 'FRESH') {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
} else if (parts[0] === 'JRS') {
const useEdgeSchema = parts.length > 1 ? parts[1] === '1' : false;
const newRep = CONVERTER.toJRS(this, {edge: useEdgeSchema});
FS.writeFileSync(filename, JRSResume.stringify( newRep ), 'utf8');
} else {
throw {badVer: safeFormat};
}
return this;
}
/**
Duplicate this FreshResume instance.
This method first extend()s this object onto an empty, creating a deep copy,
and then passes the result into a new FreshResume instance via .parseJSON.
We do it this way to create a true clone of the object without re-running any
of the associated processing.
*/
dupe() {
const jso = extend(true, { }, this);
const rnew = new FreshResume();
rnew.parseJSON(jso, { });
return rnew;
}
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way.
*/
stringify() { return FreshResume.stringify(this); }
/**
Create a copy of this resume in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder).
TODO: Move this out of FRESHResume.
*/
transformStrings( filt, transformer ) {
const ret = this.dupe();
const trx = require('../utils/string-transformer');
return trx(ret, filt, transformer);
}
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
markdownify() {
const MDIN = txt => MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
const trx = function( key, val ) {
if (key === 'summary') {
return MD(val);
}
return MDIN(val);
};
return this.transformStrings(['skills','url','start','end','date'], trx);
}
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
xmlify() {
const trx = (key, val) => XML(val);
return this.transformStrings([], trx);
}
/** Return the resume format. */
format() { return 'FRESH'; }
/**
Return internal metadata. Create if it doesn't exist.
*/
i() { return this.imp = this.imp || { }; }
/**
Return a unique list of all skills declared in the resume.
*/
// TODO: Several problems here:
// 1) Confusing name. Easily confused with the keyword-inspector module, which
// parses resume body text looking for these same keywords. This should probably
// be renamed.
//
// 2) Doesn't bother trying to integrate skills.list with skills.sets if they
// happen to declare different skills, and if skills.sets declares ONE skill and
// skills.list declared 50, only 1 skill will be registered.
//
// 3) In the future, skill.sets should only be able to use skills declared in
// skills.list. That is, skills.list is the official record of a candidate's
// declared skills. skills.sets is just a way of grouping those into skillsets
// for easier consumption.
keywords() {
let flatSkills = [];
if (this.skills) {
if (this.skills.sets) {
flatSkills = this.skills.sets.map(sk => sk.skills).reduce( (a,b) => a.concat(b));
} else if (this.skills.list) {
flatSkills = flatSkills.concat( this.skills.list.map(sk => sk.name) );
}
flatSkills = _.uniq(flatSkills);
}
return flatSkills;
}
/**
Reset the sheet to an empty state. TODO: refactor/review
*/
clear( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
if (clearMeta) { delete this.imp; }
delete this.computed; // Don't use Object.keys() here
delete this.employment;
delete this.service;
delete this.education;
delete this.recognition;
delete this.reading;
delete this.writing;
delete this.interests;
delete this.skills;
return delete this.social;
}
/**
Get a safe count of the number of things in a section.
*/
count( obj ) {
if (!obj) { return 0; }
if (obj.history) { return obj.history.length; }
if (obj.sets) { return obj.sets.length; }
return obj.length || 0;
}
/** Add work experience to the sheet. */
add( moniker ) {
const defSheet = FreshResume.default();
const newObject =
defSheet[moniker].history
? extend( true, {}, defSheet[ moniker ].history[0] )
:
moniker === 'skills'
? extend( true, {}, defSheet.skills.sets[0] )
: extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || [];
if (this[ moniker ].history) {
this[ moniker ].history.push(newObject);
} else if (moniker === 'skills') {
this.skills.sets.push(newObject);
} else {
this[ moniker ].push(newObject);
}
return newObject;
}
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
hasProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.some(this.social, p => p.network.trim().toLowerCase() === socialNetwork);
}
/** Return the specified network profile. */
getProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.find(this.social, sn => sn.network.trim().toLowerCase() === socialNetwork);
}
/**
Return an array of profiles for the specified network, for when the user
has multiple eg. GitHub accounts.
*/
getProfiles( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.filter(this.social, sn => sn.network.trim().toLowerCase() === socialNetwork);
}
/** Determine if the sheet includes a specific skill. */
hasSkill( skill ) {
skill = skill.trim().toLowerCase();
return this.skills && _.some(this.skills, sk =>
sk.keywords && _.some(sk.keywords, kw => kw.trim().toLowerCase() === skill)
);
}
/** Validate the sheet against the FRESH Resume schema. */
isValid() {
const schemaObj = require('fresh-resume-schema');
validator = require('is-my-json-valid');
const validate = validator( schemaObj, { // See Note [1].
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
const ret = validate(this);
if (!ret) {
this.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
}
return ret;
}
duration(unit) {
const inspector = require('../inspectors/duration-inspector');
return inspector.run(this, 'employment.history', 'start', 'end', unit);
}
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
sort() {
const byDateDesc = function(a,b) {
if (a.safe.start.isBefore(b.safe.start)) {
return 1;
} else { if (a.safe.start.isAfter(b.safe.start)) { return -1; } else { return 0; } }
};
const sortSection = function( key ) {
const ar = __.get(this, key);
if (ar && ar.length) {
const datedThings = ar.filter(o => o.start);
return datedThings.sort( byDateDesc );
}
};
sortSection('employment.history');
sortSection('education.history');
sortSection('service.history');
sortSection('projects');
return this.writing && this.writing.sort(function(a, b) {
if (a.safe.date.isBefore(b.safe.date)) {
return 1;
} else { return ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0; }
});
}
}
/**
Get the default (starter) sheet.
*/
FreshResume.default = () => new FreshResume().parseJSON(require('fresh-resume-starter').fresh);
/**
Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
along the way.
*/
FreshResume.stringify = function( obj ) {
const replacer = function( key,value ) { // Exclude these keys from stringification
const exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'];
if (_.some( exKeys, val => key.trim() === val)) {
return undefined; } else { return value; }
};
return JSON.stringify(obj, replacer, 2);
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
var _parseDates = function() {
const _fmt = require('./fluent-date').fmt;
const that = this;
// TODO: refactor recursion
var replaceDatesInObject = function( obj ) {
if (!obj) { return; }
if (Object.prototype.toString.call( obj ) === '[object Array]') {
obj.forEach(elem => replaceDatesInObject( elem ));
return;
} else if (typeof obj === 'object') {
if (obj._isAMomentObject || obj.safe) {
return;
}
Object.keys( obj ).forEach(key => replaceDatesInObject(obj[key]));
['start','end','date'].forEach(function(val) {
if ((obj[val] !== undefined) && (!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');
return;
}
}
});
return;
}
};
Object.keys( this ).forEach(function(member) {
replaceDatesInObject(that[member]);
});
};
/** Export the Sheet function/ctor. */
module.exports = FreshResume;
// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
// in addition to YYYY-MM-DD. The original regex:
//
// /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/
//

View File

@ -1,231 +0,0 @@
###*
Definition of the FRESHTheme class.
@module core/fresh-theme
@license MIT. See LICENSE.md for details.
###
FS = require 'fs'
validator = require 'is-my-json-valid'
_ = require 'underscore'
PATH = require 'path'
parsePath = require 'parse-filepath'
pathExists = require('path-exists').sync
EXTEND = require 'extend'
HMSTATUS = require './status-codes'
moment = require 'moment'
loadSafeJson = require '../utils/safe-json-loader'
READFILES = require 'recursive-readdir-sync'
### A representation of a FRESH theme asset.
@class FRESHTheme ###
class FRESHTheme
constructor: () ->
@baseFolder = 'src'
return
### Open and parse the specified theme. ###
open: ( themeFolder ) ->
@folder = themeFolder
# Open the [theme-name].json file; should have the same name as folder
pathInfo = parsePath themeFolder
# Set up a formats hash for the theme
formatsHash = { }
# Load the theme
themeFile = PATH.join themeFolder, 'theme.json'
themeInfo = loadSafeJson themeFile
if themeInfo.ex
throw
fluenterror:
if themeInfo.ex.op == 'parse'
then HMSTATUS.parseError
else HMSTATUS.readError
inner: themeInfo.ex.inner
that = this
# Move properties from the theme JSON file to the theme object
EXTEND true, @, themeInfo.json
# Check for an "inherits" entry in the theme JSON.
if @inherits
cached = { }
_.each @inherits, (th, key) ->
# First, see if this is one of the predefined FRESH themes. There are
# only a handful of these, but they may change over time, so we need to
# query the official source of truth: the fresh-themes repository, which
# mounts the themes conveniently by name to the module object, and which
# is embedded locally inside the HackMyResume installation.
# TODO: merge this code with
themesObj = require 'fresh-themes'
if _.has themesObj.themes, th
themePath = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/themes/',
th
)
else
d = parsePath( th ).dirname
themePath = PATH.join d, th
cached[ th ] = cached[th] || new FRESHTheme().open( themePath )
formatsHash[ key ] = cached[ th ].getFormat( key )
# Load theme files
formatsHash = _load.call @, formatsHash
# Cache
@formats = formatsHash
# Set the official theme name
@name = parsePath( @folder ).name
@
### Determine if the theme supports the specified output format. ###
hasFormat: ( fmt ) -> _.has @formats, fmt
### Determine if the theme supports the specified output format. ###
getFormat: ( fmt ) -> @formats[ fmt ]
### Load and parse theme source files. ###
_load = (formatsHash) ->
that = @
major = false
tplFolder = PATH.join @folder, @baseFolder
copyOnly = ['.ttf','.otf', '.png','.jpg','.jpeg','.pdf']
# 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.
fmts = READFILES(tplFolder).map (absPath) ->
_loadOne.call @, absPath, formatsHash, tplFolder
, @
# Now, get all the CSS files...
@cssFiles = fmts.filter (fmt) -> fmt and (fmt.ext == 'css')
# For each CSS file, get its corresponding HTML file. It's possible that
# a theme can have a CSS file but *no* HTML file, as when a theme author
# creates a pure CSS override of an existing theme.
@cssFiles.forEach (cssf) ->
idx = _.findIndex fmts, ( fmt ) ->
fmt && fmt.pre == cssf.pre && fmt.ext == 'html'
cssf.major = false
if idx > -1
fmts[ idx ].css = cssf.data
fmts[ idx ].cssPath = cssf.path
else
if that.inherits
# Found a CSS file without an HTML file in a theme that inherits
# from another theme. This is the override CSS file.
that.overrides = { file: cssf.path, data: cssf.data }
# Now, save all the javascript file paths to a theme property.
jsFiles = fmts.filter (fmt) -> fmt and (fmt.ext == 'js')
@.jsFiles = jsFiles.map (jsf) -> jsf['path']
formatsHash
### Load a single theme file. ###
_loadOne = ( absPath, formatsHash, tplFolder ) ->
pathInfo = parsePath absPath
return if pathInfo.basename.toLowerCase() == 'theme.json'
absPathSafe = absPath.trim().toLowerCase()
outFmt = ''
act = 'copy'
isPrimary = false
# If this is an "explicit" theme, all files of importance are specified in
# the "transform" section of the theme.json file.
if @explicit
outFmt = _.find Object.keys( @formats ), ( fmtKey ) ->
fmtVal = @formats[ fmtKey ]
_.some fmtVal.transform, (fpath) ->
absPathB = PATH.join( @folder, fpath ).trim().toLowerCase()
absPathB == absPathSafe
, @
, @
act = 'transform' if outFmt
if !outFmt
# If this file lives in a specific format folder within the theme,
# such as "/latex" or "/html", then that format is the implicit output
# format for all files within the folder
portion = pathInfo.dirname.replace tplFolder,''
if portion && portion.trim()
return if portion[1] == '_'
reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig
res = reg.exec( portion )
if res
if res[1] != 'partials'
outFmt = res[1]
act = 'transform' if !@explicit
else
@partials = @partials || []
@partials.push( { name: pathInfo.name, path: absPath } )
return null
# Otherwise, the output format is inferred from the filename, as in
# compact-[outputformat].[extension], for ex, compact-pdf.html
if !outFmt
idx = pathInfo.name.lastIndexOf '-'
outFmt = if idx == -1 then pathInfo.name else pathInfo.name.substr idx+1
act = 'transform' if !@explicit
defFormats = require './default-formats'
isPrimary = _.some defFormats, (form) ->
form.name == outFmt and pathInfo.extname != '.css'
# Make sure we have a valid formatsHash
formatsHash[ outFmt ] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
}
# Move symlink descriptions from theme.json to the format
if @formats?[ outFmt ]?.symLinks
formatsHash[ outFmt ].symLinks = @formats[ outFmt ].symLinks
# Create the file representation object
obj =
action: act
primary: isPrimary
path: absPath
orgPath: PATH.relative tplFolder, absPath
ext: pathInfo.extname.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 )
obj
### Return a more friendly name for certain formats. ###
friendlyName = ( val ) ->
val = (val && val.trim().toLowerCase()) || ''
friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }
friendly[val] || val
module.exports = FRESHTheme

253
src/core/fresh-theme.js Normal file
View File

@ -0,0 +1,253 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the FRESHTheme class.
@module core/fresh-theme
@license MIT. See LICENSE.md for details.
*/
const FS = require('fs');
const _ = require('underscore');
const PATH = require('path');
const parsePath = require('parse-filepath');
const EXTEND = require('extend');
const HMSTATUS = require('./status-codes');
const loadSafeJson = require('../utils/safe-json-loader');
const READFILES = require('recursive-readdir-sync');
/* A representation of a FRESH theme asset.
@class FRESHTheme */
class FRESHTheme {
constructor() {
this.baseFolder = 'src';
}
/* Open and parse the specified theme. */
open( themeFolder ) {
this.folder = themeFolder;
// Set up a formats hash for the theme
let formatsHash = { };
// Load the theme
const themeFile = PATH.join(themeFolder, 'theme.json');
const themeInfo = loadSafeJson(themeFile);
if (themeInfo.ex) {
throw{
fluenterror:
themeInfo.ex.op === 'parse'
? HMSTATUS.parseError
: HMSTATUS.readError,
inner: themeInfo.ex.inner
};
}
// Move properties from the theme JSON file to the theme object
EXTEND(true, this, themeInfo.json);
// Check for an "inherits" entry in the theme JSON.
if (this.inherits) {
const cached = { };
_.each(this.inherits, function(th, key) {
// First, see if this is one of the predefined FRESH themes. There are
// only a handful of these, but they may change over time, so we need to
// query the official source of truth: the fresh-themes repository, which
// mounts the themes conveniently by name to the module object, and which
// is embedded locally inside the HackMyResume installation.
// TODO: merge this code with
let themePath;
const themesObj = require('fresh-themes');
if (_.has(themesObj.themes, th)) {
themePath = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/themes/',
th
);
} else {
const d = parsePath( th ).dirname;
themePath = PATH.join(d, th);
}
cached[ th ] = cached[th] || new FRESHTheme().open( themePath );
return formatsHash[ key ] = cached[ th ].getFormat( key );
});
}
// Load theme files
formatsHash = _load.call(this, formatsHash);
// Cache
this.formats = formatsHash;
// Set the official theme name
this.name = parsePath( this.folder ).name;
return this;
}
/* Determine if the theme supports the specified output format. */
hasFormat( fmt ) { return _.has(this.formats, fmt); }
/* Determine if the theme supports the specified output format. */
getFormat( fmt ) { return this.formats[ fmt ]; }
}
/* Load and parse theme source files. */
var _load = function(formatsHash) {
const that = this;
const tplFolder = PATH.join(this.folder, this.baseFolder);
// 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.
const fmts = READFILES(tplFolder).map(function(absPath) {
return _loadOne.call(this, absPath, formatsHash, tplFolder);
}
, this);
// Now, get all the CSS files...
this.cssFiles = fmts.filter(fmt => fmt && (fmt.ext === 'css'));
// For each CSS file, get its corresponding HTML file. It's possible that
// a theme can have a CSS file but *no* HTML file, as when a theme author
// creates a pure CSS override of an existing theme.
this.cssFiles.forEach(function(cssf) {
const idx = _.findIndex(fmts, fmt => fmt && (fmt.pre === cssf.pre) && (fmt.ext === 'html'));
cssf.major = false;
if (idx > -1) {
fmts[ idx ].css = cssf.data;
return fmts[ idx ].cssPath = cssf.path;
} else {
if (that.inherits) {
// Found a CSS file without an HTML file in a theme that inherits
// from another theme. This is the override CSS file.
return that.overrides = { file: cssf.path, data: cssf.data };
}
}});
// Now, save all the javascript file paths to a theme property.
const jsFiles = fmts.filter(fmt => fmt && (fmt.ext === 'js'));
this.jsFiles = jsFiles.map(jsf => jsf['path']);
return formatsHash;
};
/* Load a single theme file. */
var _loadOne = function( absPath, formatsHash, tplFolder ) {
const pathInfo = parsePath(absPath);
if (pathInfo.basename.toLowerCase() === 'theme.json') { return; }
const absPathSafe = absPath.trim().toLowerCase();
let outFmt = '';
let act = 'copy';
let isPrimary = false;
// If this is an "explicit" theme, all files of importance are specified in
// the "transform" section of the theme.json file.
if (this.explicit) {
outFmt = _.find(Object.keys( this.formats ), function( fmtKey ) {
const fmtVal = this.formats[ fmtKey ];
return _.some(fmtVal.transform, function(fpath) {
const absPathB = PATH.join( this.folder, fpath ).trim().toLowerCase();
return absPathB === absPathSafe;
}
, this);
}
, this);
if (outFmt) { act = 'transform'; }
}
if (!outFmt) {
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the implicit output
// format for all files within the folder
const portion = pathInfo.dirname.replace(tplFolder,'');
if (portion && portion.trim()) {
if (portion[1] === '_') { return; }
const reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
const res = reg.exec( portion );
if (res) {
if (res[1] !== 'partials') {
outFmt = res[1];
if (!this.explicit) { act = 'transform'; }
} else {
this.partials = this.partials || [];
this.partials.push( { name: pathInfo.name, path: absPath } );
return null;
}
}
}
}
// Otherwise, the output format is inferred from the filename, as in
// compact-[outputformat].[extension], for ex, compact-pdf.html
if (!outFmt) {
const idx = pathInfo.name.lastIndexOf('-');
outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx+1);
if (!this.explicit) { act = 'transform'; }
const defFormats = require('./default-formats');
isPrimary = _.some(defFormats, form => (form.name === outFmt) && (pathInfo.extname !== '.css'));
}
// Make sure we have a valid formatsHash
formatsHash[ outFmt ] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
};
// Move symlink descriptions from theme.json to the format
if (__guard__(this.formats != null ? this.formats[outFmt ] : undefined, x => x.symLinks)) {
formatsHash[ outFmt ].symLinks = this.formats[ outFmt ].symLinks;
}
// Create the file representation object
const obj = {
action: act,
primary: isPrimary,
path: absPath,
orgPath: PATH.relative(tplFolder, absPath),
ext: pathInfo.extname.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;
};
/* Return a more friendly name for certain formats. */
var friendlyName = function( val ) {
val = (val && val.trim().toLowerCase()) || '';
const friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val;
};
module.exports = FRESHTheme;
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

View File

@ -1,304 +0,0 @@
###*
Definition of the JRSResume class.
@license MIT. See LICENSE.md for details.
@module core/jrs-resume
###
FS = require('fs')
extend = require('extend')
validator = require('is-my-json-valid')
_ = require('underscore')
PATH = require('path')
MD = require('marked')
CONVERTER = require('fresh-jrs-converter')
moment = require('moment')
###*
A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
is an instantiation of that JSON decorated with utility methods.
@class JRSResume
###
class JRSResume# extends AbstractResume
###* Initialize the the JSResume from string. ###
parse: ( stringData, opts ) ->
@imp = @imp ? raw: stringData
this.parseJSON JSON.parse( stringData ), opts
###*
Initialize the JRSResume object from JSON.
Open and parse the specified JRS resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
###
parseJSON: ( rep, opts ) ->
opts = opts || { };
if opts.privatize
scrubber = require '../utils/resume-scrubber'
# Ignore any element with the 'ignore: true' or 'private: true' designator.
{ scrubbed, ignoreList, privateList } = scrubber.scrubResume rep, opts
# Extend resume properties onto ourself.
extend true, this, if opts.privatize then scrubbed else rep
# Set up metadata
if !@imp?.processed
# Set up metadata TODO: Clean up metadata on the object model.
opts = opts || { }
if opts.imp == undefined || opts.imp
@imp = @imp || { }
@imp.title = (opts.title || @imp.title) || @basics.name
unless @imp.raw
@imp.raw = JSON.stringify rep
@imp.processed = true
# Parse dates, sort dates, and calculate computed values
(opts.date == undefined || opts.date) && _parseDates.call( this )
(opts.sort == undefined || opts.sort) && this.sort()
if opts.compute == undefined || opts.compute
@basics.computed =
numYears: this.duration()
keywords: this.keywords()
@
###* Save the sheet to disk (for environments that have disk access). ###
save: ( filename ) ->
@imp.file = filename || @imp.file
FS.writeFileSync @imp.file, @stringify( this ), 'utf8'
@
###* Save the sheet to disk in a specific format, either FRESH or JRS. ###
saveAs: ( filename, format ) ->
if format == 'JRS'
@imp.file = filename || @imp.file;
FS.writeFileSync( @imp.file, @stringify(), 'utf8' );
else
newRep = CONVERTER.toFRESH @
stringRep = CONVERTER.toSTRING newRep
FS.writeFileSync filename, stringRep, 'utf8'
@
###* Return the resume format. ###
format: () -> 'JRS'
stringify: () -> JRSResume.stringify( @ )
###* Return a unique list of all keywords across all skills. ###
keywords: () ->
flatSkills = []
if @skills && this.skills.length
@skills.forEach ( s ) -> flatSkills = _.union flatSkills, s.keywords
flatSkills
###*
Return internal metadata. Create if it doesn't exist.
JSON Resume v0.0.0 doesn't allow additional properties at the root level,
so tuck this into the .basic sub-object.
###
i: () ->
@imp = @imp ? { }
###* Reset the sheet to an empty state. ###
clear = ( clearMeta ) ->
clearMeta = ((clearMeta == undefined) && true) || clearMeta;
delete this.imp if clearMeta
delete this.basics.computed # Don't use Object.keys() here
delete this.work
delete this.volunteer
delete this.education
delete this.awards
delete this.publications
delete this.interests
delete this.skills
delete this.basics.profiles
###* Add work experience to the sheet. ###
add: ( moniker ) ->
defSheet = JRSResume.default()
newObject = $.extend( true, {}, defSheet[ moniker ][0] )
this[ moniker ] = this[ moniker ] || []
this[ moniker ].push( newObject )
newObject
###* Determine if the sheet includes a specific social profile (eg, GitHub). ###
hasProfile: ( socialNetwork ) ->
socialNetwork = socialNetwork.trim().toLowerCase()
return @basics.profiles && _.some @basics.profiles, (p) ->
return p.network.trim().toLowerCase() == socialNetwork
###* Determine if the sheet includes a specific skill. ###
hasSkill: ( skill ) ->
skill = skill.trim().toLowerCase()
return this.skills && _.some this.skills, (sk) ->
return sk.keywords && _.some sk.keywords, (kw) ->
kw.trim().toLowerCase() == skill
###* Validate the sheet against the JSON Resume schema. ###
isValid: ( ) -> # TODO: ↓ fix this path ↓
schema = FS.readFileSync PATH.join( __dirname, 'resume.json' ), 'utf8'
schemaObj = JSON.parse schema
validator = require 'is-my-json-valid'
validate = validator( schemaObj, { # Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
temp = @imp
delete @imp
ret = validate @
@imp = temp
if !ret
@imp = @imp || { };
@imp.validationErrors = validate.errors;
ret
duration: (unit) ->
inspector = require '../inspectors/duration-inspector';
inspector.run @, 'work', 'startDate', 'endDate', unit
###*
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
###
sort: ( ) ->
byDateDesc = (a,b) ->
if a.safeStartDate.isBefore(b.safeStartDate)
then 1
else ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0
@work && @work.sort byDateDesc
@education && @education.sort byDateDesc
@volunteer && @volunteer.sort byDateDesc
@awards && @awards.sort (a, b) ->
if a.safeDate.isBefore b.safeDate
then 1
else (a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
@publications && @publications.sort (a, b) ->
if ( a.safeReleaseDate.isBefore(b.safeReleaseDate) )
then 1
else ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0
dupe: () ->
rnew = new JRSResume()
rnew.parse this.stringify(), { }
rnew
###*
Create a copy of this resume in which all fields have been interpreted as
Markdown.
###
harden: () ->
ret = @dupe()
HD = (txt) -> '@@@@~' + txt + '~@@@@'
HDIN = (txt) ->
#return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
return HD txt
transformer = require '../utils/string-transformer'
transformer ret,
[ 'skills','url','website','startDate','endDate', 'releaseDate', 'date',
'phone','email','address','postalCode','city','country','region',
'safeStartDate','safeEndDate' ],
(key, val) -> HD val
###* Get the default (empty) sheet. ###
JRSResume.default = () ->
new JRSResume().parseJSON require('fresh-resume-starter').jrs
###*
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
###
JRSResume.stringify = ( obj ) ->
replacer = ( key,value ) -> # Exclude these keys from stringification
temp = _.some ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview', 'display_progress_bar'],
( val ) -> return key.trim() == val
return if temp then undefined else value
JSON.stringify obj, replacer, 2
###*
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
###
_parseDates = () ->
_fmt = require('./fluent-date').fmt
@work && @work.forEach (job) ->
job.safeStartDate = _fmt( job.startDate )
job.safeEndDate = _fmt( job.endDate )
@education && @education.forEach (edu) ->
edu.safeStartDate = _fmt( edu.startDate )
edu.safeEndDate = _fmt( edu.endDate )
@volunteer && @volunteer.forEach (vol) ->
vol.safeStartDate = _fmt( vol.startDate )
vol.safeEndDate = _fmt( vol.endDate )
@awards && @awards.forEach (awd) ->
awd.safeDate = _fmt( awd.date )
@publications && @publications.forEach (pub) ->
pub.safeReleaseDate = _fmt( pub.releaseDate )
###*
Export the JRSResume function/ctor.
###
module.exports = JRSResume

348
src/core/jrs-resume.js Normal file
View File

@ -0,0 +1,348 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the JRSResume class.
@license MIT. See LICENSE.md for details.
@module core/jrs-resume
*/
const FS = require('fs');
const extend = require('extend');
let validator = require('is-my-json-valid');
const _ = require('underscore');
const PATH = require('path');
const CONVERTER = require('fresh-jrs-converter');
/**
A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
is an instantiation of that JSON decorated with utility methods.
@class JRSResume
*/
class JRSResume {
static initClass() {
/** Reset the sheet to an empty state. */
// clear = function( clearMeta ) {
// clearMeta = ((clearMeta === undefined) && true) || clearMeta;
// if (clearMeta) { delete this.imp; }
// delete this.basics.computed; // Don't use Object.keys() here
// delete this.work;
// delete this.volunteer;
// delete this.education;
// delete this.awards;
// delete this.publications;
// delete this.interests;
// delete this.skills;
// return delete this.basics.profiles;
// };
// extends AbstractResume
}
/** Initialize the the JSResume from string. */
parse( stringData, opts ) {
this.imp = this.imp != null ? this.imp : {raw: stringData};
return this.parseJSON(JSON.parse( stringData ), opts);
}
/**
Initialize the JRSResume object from JSON.
Open and parse the specified JRS resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
*/
parseJSON( rep, opts ) {
let scrubbed;
opts = opts || { };
if (opts.privatize) {
const scrubber = require('../utils/resume-scrubber');
// Ignore any element with the 'ignore: true' or 'private: true' designator.
var ret = scrubber.scrubResume(rep, opts);
scrubbed = ret.scrubbed;
}
// Extend resume properties onto ourself.
extend(true, this, opts.privatize ? scrubbed : rep);
// Set up metadata
if (!(this.imp != null ? this.imp.processed : undefined)) {
// Set up metadata TODO: Clean up metadata on the object model.
opts = opts || { };
if ((opts.imp === undefined) || opts.imp) {
this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.basics.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
}
}
this.imp.processed = true;
}
// Parse dates, sort dates, and calculate computed values
((opts.date === undefined) || opts.date) && _parseDates.call( this );
((opts.sort === undefined) || opts.sort) && this.sort();
if ((opts.compute === undefined) || opts.compute) {
this.basics.computed = {
numYears: this.duration(),
keywords: this.keywords()
};
}
return this;
}
/** Save the sheet to disk (for environments that have disk access). */
save( filename ) {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify( this ), 'utf8');
return this;
}
/** Save the sheet to disk in a specific format, either FRESH or JRS. */
saveAs( filename, format ) {
if (format === 'JRS') {
this.imp.file = filename || this.imp.file;
FS.writeFileSync( this.imp.file, this.stringify(), 'utf8' );
} else {
const newRep = CONVERTER.toFRESH(this);
const stringRep = CONVERTER.toSTRING(newRep);
FS.writeFileSync(filename, stringRep, 'utf8');
}
return this;
}
/** Return the resume format. */
format() { return 'JRS'; }
stringify() { return JRSResume.stringify( this ); }
/** Return a unique list of all keywords across all skills. */
keywords() {
let flatSkills = [];
if (this.skills && this.skills.length) {
this.skills.forEach( s => flatSkills = _.union(flatSkills, s.keywords));
}
return flatSkills;
}
/**
Return internal metadata. Create if it doesn't exist.
JSON Resume v0.0.0 doesn't allow additional properties at the root level,
so tuck this into the .basic sub-object.
*/
i() {
return this.imp = this.imp != null ? this.imp : { };
}
/** Add work experience to the sheet. */
add( moniker ) {
const defSheet = JRSResume.default();
const newObject = extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject );
return newObject;
}
/** Determine if the sheet includes a specific social profile (eg, GitHub). */
hasProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.basics.profiles && _.some(this.basics.profiles, p => p.network.trim().toLowerCase() === socialNetwork);
}
/** Determine if the sheet includes a specific skill. */
hasSkill( skill ) {
skill = skill.trim().toLowerCase();
return this.skills && _.some(this.skills, sk =>
sk.keywords && _.some(sk.keywords, kw => kw.trim().toLowerCase() === skill)
);
}
/** Validate the sheet against the JSON Resume schema. */
isValid( ) { // TODO: ↓ fix this path ↓
const schema = FS.readFileSync(PATH.join( __dirname, 'resume.json' ), 'utf8');
const schemaObj = JSON.parse(schema);
validator = require('is-my-json-valid');
const validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
const temp = this.imp;
delete this.imp;
const ret = validate(this);
this.imp = temp;
if (!ret) {
this.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
}
return ret;
}
duration(unit) {
const inspector = require('../inspectors/duration-inspector');
return inspector.run(this, 'work', 'startDate', 'endDate', unit);
}
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
sort( ) {
const byDateDesc = function(a,b) {
if (a.safeStartDate.isBefore(b.safeStartDate)) {
return 1;
} else { return ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0; }
};
this.work && this.work.sort(byDateDesc);
this.education && this.education.sort(byDateDesc);
this.volunteer && this.volunteer.sort(byDateDesc);
this.awards && this.awards.sort(function(a, b) {
if (a.safeDate.isBefore(b.safeDate)) {
return 1;
} else { return (a.safeDate.isAfter(b.safeDate) && -1 ) || 0; }
});
return this.publications && this.publications.sort(function(a, b) {
if ( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) {
return 1;
} else { return ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0; }
});
}
dupe() {
const rnew = new JRSResume();
rnew.parse(this.stringify(), { });
return rnew;
}
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
harden() {
const ret = this.dupe();
const HD = txt => `@@@@~${txt}~@@@@`;
// const HDIN = txt =>
// //return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
// HD(txt)
// ;
const transformer = require('../utils/string-transformer');
return transformer(ret,
[ 'skills','url','website','startDate','endDate', 'releaseDate', 'date',
'phone','email','address','postalCode','city','country','region',
'safeStartDate','safeEndDate' ],
(key, val) => HD(val));
}
}
JRSResume.initClass();
/** Get the default (empty) sheet. */
JRSResume.default = () => new JRSResume().parseJSON(require('fresh-resume-starter').jrs);
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
JRSResume.stringify = function( obj ) {
const replacer = function( key,value ) { // Exclude these keys from stringification
const temp = _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview', 'display_progress_bar'],
val => key.trim() === val);
if (temp) { return undefined; } else { return value; }
};
return JSON.stringify(obj, replacer, 2);
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
var _parseDates = function() {
const _fmt = require('./fluent-date').fmt;
this.work && this.work.forEach(function(job) {
job.safeStartDate = _fmt( job.startDate );
return job.safeEndDate = _fmt( job.endDate );
});
this.education && this.education.forEach(function(edu) {
edu.safeStartDate = _fmt( edu.startDate );
return edu.safeEndDate = _fmt( edu.endDate );
});
this.volunteer && this.volunteer.forEach(function(vol) {
vol.safeStartDate = _fmt( vol.startDate );
return vol.safeEndDate = _fmt( vol.endDate );
});
this.awards && this.awards.forEach(awd => awd.safeDate = _fmt( awd.date ));
return this.publications && this.publications.forEach(pub => pub.safeReleaseDate = _fmt( pub.releaseDate ));
};
/**
Export the JRSResume class.
*/
module.exports = JRSResume;

View File

@ -1,86 +0,0 @@
###*
Definition of the JRSTheme class.
@module core/jrs-theme
@license MIT. See LICENSE.MD for details.
###
_ = require 'underscore'
PATH = require 'path'
parsePath = require 'parse-filepath'
pathExists = require('path-exists').sync
errors = require './status-codes'
###*
The JRSTheme class is a representation of a JSON Resume theme asset.
@class JRSTheme
###
class JRSTheme
###*
Open and parse the specified JRS theme.
@method open
###
open: ( thFolder ) ->
@folder = thFolder
pathInfo = parsePath thFolder
# Open and parse the theme's package.json file
pkgJsonPath = PATH.join thFolder, 'package.json'
if pathExists pkgJsonPath
thApi = require thFolder # Requiring the folder yields whatever the package.json's "main" is set to
thPkg = require pkgJsonPath # Get the package.json as JSON
this.name = thPkg.name
this.render = (thApi && thApi.render) || undefined
this.engine = 'jrs'
# Create theme formats (HTML and PDF). Just add the bare minimum mix of
# properties necessary to allow JSON Resume themes to share a rendering
# path with FRESH themes.
this.formats =
html:
outFormat: 'html'
files: [{
action: 'transform',
render: this.render,
primary: true,
ext: 'html',
css: null
}]
pdf:
outFormat: 'pdf'
files: [{
action: 'transform',
render: this.render,
primary: true,
ext: 'pdf',
css: null
}]
else
throw fluenterror: errors.missingPackageJSON
@
###*
Determine if the theme supports the output format.
@method hasFormat
###
hasFormat: ( fmt ) -> _.has this.formats, fmt
###*
Return the requested output format.
@method getFormat
###
getFormat: ( fmt ) -> @formats[ fmt ]
module.exports = JRSTheme;

96
src/core/jrs-theme.js Normal file
View File

@ -0,0 +1,96 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the JRSTheme class.
@module core/jrs-theme
@license MIT. See LICENSE.MD for details.
*/
const _ = require('underscore');
const PATH = require('path');
const pathExists = require('path-exists').sync;
const errors = require('./status-codes');
/**
The JRSTheme class is a representation of a JSON Resume theme asset.
@class JRSTheme
*/
class JRSTheme {
/**
Open and parse the specified JRS theme.
@method open
*/
open( thFolder ) {
this.folder = thFolder;
//const pathInfo = parsePath(thFolder);
// Open and parse the theme's package.json file
const pkgJsonPath = PATH.join(thFolder, 'package.json');
if (pathExists(pkgJsonPath)) {
const thApi = require(thFolder); // Requiring the folder yields whatever the package.json's "main" is set to
const thPkg = require(pkgJsonPath); // Get the package.json as JSON
this.name = thPkg.name;
this.render = (thApi && thApi.render) || undefined;
this.engine = 'jrs';
// Create theme formats (HTML and PDF). Just add the bare minimum mix of
// properties necessary to allow JSON Resume themes to share a rendering
// path with FRESH themes.
this.formats = {
html: {
outFormat: 'html',
files: [{
action: 'transform',
render: this.render,
primary: true,
ext: 'html',
css: null
}]
},
pdf: {
outFormat: 'pdf',
files: [{
action: 'transform',
render: this.render,
primary: true,
ext: 'pdf',
css: null
}]
}
};
} else {
throw {fluenterror: errors.missingPackageJSON};
}
return this;
}
/**
Determine if the theme supports the output format.
@method hasFormat
*/
hasFormat( fmt ) { return _.has(this.formats, fmt); }
/**
Return the requested output format.
@method getFormat
*/
getFormat( fmt ) { return this.formats[ fmt ]; }
}
module.exports = JRSTheme;

View File

@ -1,112 +0,0 @@
###*
Definition of the ResumeFactory class.
@license MIT. See LICENSE.md for details.
@module core/resume-factory
###
FS = require 'fs'
HMS = require './status-codes'
HME = require './event-codes'
ResumeConverter = require 'fresh-jrs-converter'
chalk = require 'chalk'
SyntaxErrorEx = require '../utils/syntax-error-ex'
_ = require 'underscore'
resumeDetect = require '../utils/resume-detector'
require 'string.prototype.startswith'
###*
A simple factory class for FRESH and JSON Resumes.
@class ResumeFactory
###
ResumeFactory = module.exports =
###*
Load one or more resumes from disk.
@param {Object} opts An options object with settings for the factory as well
as passthrough settings for FRESHResume or JRSResume. Structure:
{
format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null)
objectify: true, // FRESH/JRSResume or raw JSON?
inner: { // Passthru options for FRESH/JRSResume
sort: false
}
}
###
load: ( sources, opts, emitter ) ->
sources.map( (src) ->
@loadOne( src, opts, emitter )
, @)
###* Load a single resume from disk. ###
loadOne: ( src, opts, emitter ) ->
toFormat = opts.format # Can be null
# Get the destination format. Can be 'fresh', 'jrs', or null/undefined.
toFormat && (toFormat = toFormat.toLowerCase().trim())
# Load and parse the resume JSON
info = _parse src, opts, emitter
return info if info.fluenterror
# Determine the resume format: FRESH or JRS
json = info.json
orgFormat = resumeDetect json
if orgFormat == 'unk'
info.fluenterror = HMS.unknownSchema
return info
# Convert between formats if necessary
if toFormat and ( orgFormat != toFormat )
json = ResumeConverter[ 'to' + toFormat.toUpperCase() ] json
# Objectify the resume, that is, convert it from JSON to a FRESHResume
# or JRSResume object.
rez = null
if opts.objectify
reqLib = '../core/' + (toFormat || orgFormat) + '-resume'
ResumeClass = require reqLib
rez = new ResumeClass().parseJSON( json, opts.inner )
rez.i().file = src
file: src
json: info.json
rez: rez
_parse = ( fileName, opts, eve ) ->
rawData = null
try
# Read the file
eve && eve.stat( HME.beforeRead, { file: fileName });
rawData = FS.readFileSync( fileName, 'utf8' );
eve && eve.stat( HME.afterRead, { file: fileName, data: rawData });
# Parse the file
eve && eve.stat HME.beforeParse, { data: rawData }
ret = { json: JSON.parse( rawData ) }
orgFormat =
if ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@')
then 'fresh' else 'jrs'
eve && eve.stat HME.afterParse, { file: fileName, data: ret.json, fmt: orgFormat }
return ret
catch err
# Can be ENOENT, EACCES, SyntaxError, etc.
fluenterror: if rawData then HMS.parseError else HMS.readError
inner: err
raw: rawData
file: fileName

127
src/core/resume-factory.js Normal file
View File

@ -0,0 +1,127 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the ResumeFactory class.
@license MIT. See LICENSE.md for details.
@module core/resume-factory
*/
const FS = require('fs');
const HMS = require('./status-codes');
const HME = require('./event-codes');
const ResumeConverter = require('fresh-jrs-converter');
const resumeDetect = require('../utils/resume-detector');
require('string.prototype.startswith');
/**
A simple factory class for FRESH and JSON Resumes.
@class ResumeFactory
*/
module.exports = {
/**
Load one or more resumes from disk.
@param {Object} opts An options object with settings for the factory as well
as passthrough settings for FRESHResume or JRSResume. Structure:
{
format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null)
objectify: true, // FRESH/JRSResume or raw JSON?
inner: { // Passthru options for FRESH/JRSResume
sort: false
}
}
*/
load( sources, opts, emitter ) {
return sources.map( function(src) {
return this.loadOne( src, opts, emitter );
}
, this);
},
/** Load a single resume from disk. */
loadOne( src, opts, emitter ) {
let toFormat = opts.format; // Can be null
// Get the destination format. Can be 'fresh', 'jrs', or null/undefined.
toFormat && (toFormat = toFormat.toLowerCase().trim());
// Load and parse the resume JSON
const info = _parse(src, opts, emitter);
if (info.fluenterror) { return info; }
// Determine the resume format: FRESH or JRS
let { json } = info;
const orgFormat = resumeDetect(json);
if (orgFormat === 'unk') {
info.fluenterror = HMS.unknownSchema;
return info;
}
// Convert between formats if necessary
if (toFormat && ( orgFormat !== toFormat )) {
json = ResumeConverter[ `to${toFormat.toUpperCase()}` ](json);
}
// Objectify the resume, that is, convert it from JSON to a FRESHResume
// or JRSResume object.
let rez = null;
if (opts.objectify) {
const reqLib = `../core/${toFormat || orgFormat}-resume`;
const ResumeClass = require(reqLib);
rez = new ResumeClass().parseJSON( json, opts.inner );
rez.i().file = src;
}
return {
file: src,
json: info.json,
rez
};
}
};
var _parse = function( fileName, opts, eve ) {
let rawData = null;
try {
// Read the file
eve && eve.stat( HME.beforeRead, { file: fileName });
rawData = FS.readFileSync( fileName, 'utf8' );
eve && eve.stat( HME.afterRead, { file: fileName, data: rawData });
// Parse the file
eve && eve.stat(HME.beforeParse, { data: rawData });
const ret = { json: JSON.parse( rawData ) };
const orgFormat =
ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@')
? 'fresh' : 'jrs';
eve && eve.stat(HME.afterParse, { file: fileName, data: ret.json, fmt: orgFormat });
return ret;
} catch (err) {
// Can be ENOENT, EACCES, SyntaxError, etc.
return {
fluenterror: rawData ? HMS.parseError : HMS.readError,
inner: err,
raw: rawData,
file: fileName
};
}
};

Some files were not shown because too many files have changed in this diff Show More