From 8a46d642e56b130ed15699f35a103c5b9712de71 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 13 Feb 2018 20:43:42 -0500 Subject: [PATCH] chore: decaffeinate: convert error.coffee and 58 other files to JS --- src/cli/error.js | 456 +++++++------ src/cli/main.js | 559 ++++++++-------- src/cli/msg.js | 10 +- src/cli/out.js | 278 ++++---- src/core/default-formats.js | 8 +- src/core/default-options.js | 20 +- src/core/event-codes.js | 65 +- src/core/fluent-date.js | 116 ++-- src/core/fresh-resume.js | 592 ++++++++-------- src/core/fresh-theme.js | 371 ++++++----- src/core/jrs-resume.js | 528 ++++++++------- src/core/jrs-theme.js | 91 +-- src/core/resume-factory.js | 150 +++-- src/core/status-codes.js | 69 +- src/generators/base-generator.js | 41 +- src/generators/html-generator.js | 43 +- src/generators/html-pdf-cli-generator.js | 142 ++-- src/generators/html-png-generator.js | 68 +- src/generators/json-generator.js | 40 +- src/generators/json-yaml-generator.js | 39 +- src/generators/latex-generator.js | 16 +- src/generators/markdown-generator.js | 16 +- src/generators/template-generator.js | 356 +++++----- src/generators/text-generator.js | 16 +- src/generators/word-generator.js | 12 +- src/generators/xml-generator.js | 14 +- src/generators/yaml-generator.js | 16 +- src/helpers/block-helpers.js | 88 +-- src/helpers/console-helpers.js | 86 ++- src/helpers/generic-helpers.js | 816 ++++++++++++----------- src/helpers/handlebars-helpers.js | 133 ++-- src/helpers/underscore-helpers.js | 43 +- src/index.js | 69 +- src/inspectors/duration-inspector.js | 75 ++- src/inspectors/gap-inspector.js | 210 +++--- src/inspectors/keyword-inspector.js | 106 +-- src/inspectors/totals-inspector.js | 53 +- src/renderers/handlebars-generator.js | 150 +++-- src/renderers/jrs-generator.js | 66 +- src/renderers/underscore-generator.js | 119 ++-- src/utils/file-contains.js | 12 +- src/utils/fresh-version-regex.js | 42 +- src/utils/html-to-wpml.js | 87 ++- src/utils/md2chalk.js | 24 +- src/utils/rasterize.js | 107 +-- src/utils/resume-detector.js | 25 +- src/utils/resume-scrubber.js | 80 ++- src/utils/safe-json-loader.js | 44 +- src/utils/safe-spawn.js | 59 +- src/utils/string-transformer.js | 77 ++- src/utils/string.js | 14 +- src/utils/syntax-error-ex.js | 47 +- src/verbs/analyze.js | 117 ++-- src/verbs/build.js | 608 +++++++++-------- src/verbs/convert.js | 241 ++++--- src/verbs/create.js | 113 ++-- src/verbs/peek.js | 126 ++-- src/verbs/validate.js | 144 ++-- src/verbs/verb.js | 131 ++-- 59 files changed, 4568 insertions(+), 3676 deletions(-) diff --git a/src/cli/error.js b/src/cli/error.js index 347bd5e..bea97c4 100644 --- a/src/cli/error.js +++ b/src/cli/error.js @@ -1,268 +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. -### +*/ -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' +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 = +/** 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 - @ + init( debug, assert, silent ) { + this.debug = debug; + this.assert = assert; + this.silent = silent; + this.msgs = require('./msg').errors; + return this; + }, - err: ( ex, shouldExit ) -> + err( ex, shouldExit ) { - # Short-circuit logging output if --silent is on - o = if @silent then () -> else _defaultLog + // Short-circuit logging output if --silent is on + let stack; + const o = this.silent ? function() {} : _defaultLog; - # Special case; can probably be removed. - throw ex if ex.pass + // Special case; can probably be removed. + if (ex.pass) { throw ex; } - # Load error messages - @msgs = @msgs || require('./msg').errors + // Load error messages + this.msgs = this.msgs || require('./msg').errors; - # Handle packaged HMR exceptions - if ex.fluenterror + // Handle packaged HMR exceptions + if (ex.fluenterror) { - # Output the error message - objError = assembleError.call @, ex - o( @[ 'format_' + objError.etype ]( objError.msg )) + // Output the error message + const objError = assembleError.call(this, ex); + o( this[ `format_${objError.etype}` ]( objError.msg )); - # Output the stack (sometimes) - if objError.withStack + // 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 + // 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 - stackTrace = ex.stack || (ex.inner && ex.inner.stack) - if stackTrace && this.debug - o M2C(ex.stack || ex.inner.stack, 'gray') + // 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 || '' - chalk.red.bold( if msg.toUpperCase().startsWith('ERROR:') then msg else 'Error: ' + msg ) + format_error( msg ) { + msg = msg || ''; + return chalk.red.bold( msg.toUpperCase().startsWith('ERROR:') ? msg : `Error: ${msg}` ); + }, - format_warning: ( brief, msg ) -> - chalk.yellow(brief) + chalk.yellow(msg || '') + format_warning( brief, msg ) { + return chalk.yellow(brief) + chalk.yellow(msg || ''); + }, - format_custom: ( msg ) -> msg + format_custom( msg ) { return msg; } +}; -_defaultLog = () -> console.log.apply console.log, arguments # eslint-disable-line no-console +var _defaultLog = function() { return console.log.apply(console.log, arguments); }; // eslint-disable-line no-console -assembleError = ( ex ) -> +var assembleError = function( ex ) { - msg = '' - withStack = false - quit = false - etype = 'warning' - withStack = true if @debug + let se; + let msg = ''; + let withStack = false; + let quit = false; + let etype = 'warning'; + if (this.debug) { withStack = true; } - switch ex.fluenterror + switch (ex.fluenterror) { - when HMSTATUS.themeNotFound - msg = printf( M2C( this.msgs.themeNotFound.msg, 'yellow' ), ex.data) + case HMSTATUS.themeNotFound: + msg = printf( M2C( this.msgs.themeNotFound.msg, 'yellow' ), ex.data); + break; - when HMSTATUS.copyCSS - msg = M2C( this.msgs.copyCSS.msg, 'red' ) - quit = false + case HMSTATUS.copyCSS: + msg = M2C( this.msgs.copyCSS.msg, 'red' ); + quit = false; + break; - when HMSTATUS.resumeNotFound - #msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' ); + case HMSTATUS.resumeNotFound: + //msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' ); msg += M2C(FS.readFileSync( - PATH.resolve(__dirname, 'help/' + ex.verb + '.txt'), 'utf8' ), 'white', 'yellow') + PATH.resolve(__dirname, `help/${ex.verb}.txt`), 'utf8' ), 'white', 'yellow'); + break; - 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"); + 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') + PATH.resolve(__dirname, 'help/use.txt'), 'utf8' ), 'white', 'yellow'); + break; - when HMSTATUS.invalidCommand - msg = printf( M2C( this.msgs.invalidCommand.msg, 'yellow'), ex.attempted ) + case HMSTATUS.invalidCommand: + msg = printf( M2C( this.msgs.invalidCommand.msg, 'yellow'), ex.attempted ); + break; - when HMSTATUS.resumeNotFoundAlt - msg = M2C( this.msgs.resumeNotFoundAlt.msg, 'yellow' ) + case HMSTATUS.resumeNotFoundAlt: + msg = M2C( this.msgs.resumeNotFoundAlt.msg, 'yellow' ); + break; - when HMSTATUS.inputOutputParity - msg = M2C( this.msgs.inputOutputParity.msg ) + case HMSTATUS.inputOutputParity: + msg = M2C( this.msgs.inputOutputParity.msg ); + break; - when HMSTATUS.createNameMissing - msg = M2C( this.msgs.createNameMissing.msg ) + case HMSTATUS.createNameMissing: + msg = M2C( this.msgs.createNameMissing.msg ); + break; - when HMSTATUS.pdfGeneration - msg = M2C( this.msgs.pdfGeneration.msg, 'bold' ) - msg += chalk.red('\n' + ex.inner) if ex.inner - quit = false - etype = 'error' + case HMSTATUS.pdfGeneration: + msg = M2C( this.msgs.pdfGeneration.msg, 'bold' ); + if (ex.inner) { msg += chalk.red(`\n${ex.inner}`); } + quit = false; + etype = 'error'; + break; - when HMSTATUS.invalid - msg = M2C( this.msgs.invalid.msg, 'red' ) - etype = 'error' + case HMSTATUS.invalid: + msg = M2C( this.msgs.invalid.msg, 'red' ); + etype = 'error'; + break; - when HMSTATUS.generateError - msg = (ex.inner && ex.inner.toString()) || ex - quit = false - etype = 'error' + case HMSTATUS.generateError: + msg = (ex.inner && ex.inner.toString()) || ex; + quit = false; + etype = 'error'; + break; - when HMSTATUS.fileSaveError - msg = printf( M2C( this.msgs.fileSaveError.msg ), (ex.inner || ex).toString() ) - etype = 'error' - quit = false + case HMSTATUS.fileSaveError: + msg = printf( M2C( this.msgs.fileSaveError.msg ), (ex.inner || ex).toString() ); + etype = 'error'; + quit = false; + break; - when HMSTATUS.invalidFormat - ex.data.forEach( (d) -> - msg += printf( M2C( this.msgs.invalidFormat.msg, 'bold' ), - ex.theme.name.toUpperCase(), d.format.toUpperCase()) - , @); + 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; - when HMSTATUS.missingParam - msg = printf(M2C( this.msgs.missingParam.msg ), ex.expected, ex.helper) + case HMSTATUS.missingParam: + msg = printf(M2C( this.msgs.missingParam.msg ), ex.expected, ex.helper); + break; - 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' + 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; - when HMSTATUS.notOnPath - msg = printf( M2C(this.msgs.notOnPath.msg, 'bold'), ex.engine) - quit = false - etype = 'error' + case HMSTATUS.notOnPath: + msg = printf( M2C(this.msgs.notOnPath.msg, 'bold'), ex.engine); + quit = false; + etype = 'error'; + break; - 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' + 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; - when HMSTATUS.mixedMerge - msg = M2C this.msgs.mixedMerge.msg - quit = false + case HMSTATUS.mixedMerge: + msg = M2C(this.msgs.mixedMerge.msg); + quit = false; + break; - when HMSTATUS.invokeTemplate - msg = M2C this.msgs.invokeTemplate.msg, 'red' - msg += M2C( '\n' + WRAP(ex.inner.toString(), { width: 60, indent: ' ' }), 'gray' ); - etype = 'custom' + case HMSTATUS.invokeTemplate: + msg = M2C(this.msgs.invokeTemplate.msg, 'red'); + msg += M2C( `\n${WRAP(ex.inner.toString(), { width: 60, indent: ' ' })}`, 'gray' ); + etype = 'custom'; + break; - when HMSTATUS.compileTemplate - etype = 'error' + case HMSTATUS.compileTemplate: + etype = 'error'; + break; - when HMSTATUS.themeLoad + 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' + if (ex.inner && ex.inner.fluenterror) { + msg += M2C('\nError: ', 'red') + assembleError.call( this, ex.inner ).msg; + } + quit = true; + etype = 'custom'; + break; - 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' + 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; - when HMSTATUS.createError - # inner.code could be EPERM, EACCES, etc - msg = printf M2C( this.msgs.createError.msg ), ex.inner.path - etype = 'error' + case HMSTATUS.createError: + // inner.code could be EPERM, EACCES, etc + msg = printf(M2C( this.msgs.createError.msg ), ex.inner.path); + etype = 'error'; + break; - when HMSTATUS.validateError - msg = printf M2C( @msgs.validateError.msg ), ex.inner.toString() - etype = 'error' + case HMSTATUS.validateError: + msg = printf(M2C( this.msgs.validateError.msg ), ex.inner.toString()); + etype = 'error'; + break; - 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' + 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; - when HMSTATUS.optionsFileNotFound - msg = M2C( @msgs.optionsFileNotFound.msg ) - etype = 'error' + case HMSTATUS.optionsFileNotFound: + msg = M2C( this.msgs.optionsFileNotFound.msg ); + etype = 'error'; + break; - when HMSTATUS.unknownSchema - msg = M2C( @msgs.unknownSchema.msg[0] ) - #msg += "\n" + M2C( @msgs.unknownSchema.msg[1], 'yellow' ) - etype = 'error' + case HMSTATUS.unknownSchema: + msg = M2C( this.msgs.unknownSchema.msg[0] ); + //msg += "\n" + M2C( @msgs.unknownSchema.msg[1], 'yellow' ) + etype = 'error'; + break; - when HMSTATUS.themeHelperLoad - msg = printf M2C( @msgs.themeHelperLoad.msg ), ex.glob - etype = 'error' + case HMSTATUS.themeHelperLoad: + msg = printf(M2C( this.msgs.themeHelperLoad.msg ), ex.glob); + etype = 'error'; + break; - when HMSTATUS.invalidSchemaVersion - msg = printf M2C( @msgs.invalidSchemaVersion.msg ), ex.data - etype = 'error' + case HMSTATUS.invalidSchemaVersion: + msg = printf(M2C( this.msgs.invalidSchemaVersion.msg ), ex.data); + etype = 'error'; + break; + } - msg: msg # The error message to display - withStack: withStack # Whether to include the stack - quit: quit - etype: etype + return { + msg, // The error message to display + withStack, // Whether to include the stack + quit, + etype + }; +}; diff --git a/src/cli/main.js b/src/cli/main.js index 47370d7..9126b2a 100644 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -1,51 +1,57 @@ -###* +/* + * 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. -### +*/ -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 +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 = ( rawArgs, exitCallback ) -> +*/ +module.exports = function( rawArgs, exitCallback ) { - initInfo = initialize( rawArgs, exitCallback ) - if initInfo is null - return + const initInfo = initialize( rawArgs, exitCallback ); + if (initInfo === null) { + return; + } - args = initInfo.args + const { args } = initInfo; - # Create the top-level (application) command... - program = new Command('hackmyresume') + // 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') @@ -55,65 +61,61 @@ module.exports = ( rawArgs, exitCallback ) -> .option('-a --assert', 'Treat warnings as errors', false) .option('-v --version', 'Show the version') .allowUnknownOption(); - program.jsonArgs = initInfo.options + program.jsonArgs = initInfo.options; - # Create the NEW command + // Create the NEW command program - .command 'new' - .arguments '' - .option '-f --format ', '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 - ) + .command('new') + .arguments('') + .option('-f --format ', '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 + // Create the VALIDATE command program .command('validate') .arguments('') .description('Validate a resume in FRESH or JSON RESUME format.') - .action((sources) -> - execute.call( this, sources, [], this.opts(), logMsg) - return - ) + .action(function(sources) { + execute.call( this, sources, [], this.opts(), logMsg); + }); - # Create the CONVERT command + // Create the CONVERT command program .command('convert') .description('Convert a resume to/from FRESH or JSON RESUME format.') .option('-f --format ', 'FRESH or JRS format and optional version', undefined) - .action(-> - x = splitSrcDest.call( this ); - execute.call( this, x.src, x.dst, this.opts(), logMsg) - return - ) + .action(function() { + const x = splitSrcDest.call( this ); + execute.call( this, x.src, x.dst, this.opts(), logMsg); + }); - # Create the ANALYZE command + // Create the ANALYZE command program .command('analyze') .arguments('') .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 - ) + .action(function( sources ) { + execute.call( this, sources, [], this.opts(), logMsg); + }); - # Create the PEEK command + // Create the PEEK command program .command('peek') .arguments('') .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 - ) + //.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 + // Create the BUILD command program .command('build') .alias('generate') @@ -126,134 +128,150 @@ module.exports = ( rawArgs, exitCallback ) -> .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 - ) + //.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 + // 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 + .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 ) + program.parse( args ); - if !program.args.length - throw fluenterror: 4 + if (!program.args.length) { + throw {fluenterror: 4}; + } +}; -### Massage command-line args and setup Commander.js. ### -initialize = ( ar, exitCallback ) -> +/* Massage command-line args and setup Commander.js. */ +var initialize = function( 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, + _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 ) + }); + } 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('') + // 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 + _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 + // 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 = () -> - if this.name() != 'help' - _err.err - verb: @name() + // Override the .missingArgument behavior + Command.prototype.missingArgument = function() { + if (this.name() !== 'help') { + _err.err({ + verb: this.name(), fluenterror: HMSTATUS.resumeNotFound - , true - return + } + , true); + } + }; - # Override the .helpInformation behavior - Command.prototype.helpInformation = -> - manPage = FS.readFileSync( - PATH.join(__dirname, 'help/use.txt'), 'utf8' ) - return M2C(manPage, 'white', 'yellow') + // 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; + } + } + } + } } - - -### 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' + // 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, @@ -262,129 +280,142 @@ initOptions = ( ar ) -> assert: isAssert, noescape: isNoEscape, orgVerb: oVerb, - verb: verb, + verb, json: oJSON, - args: args + 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); +}; -### 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 = () -> - # 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 ) -> +*/ +var loadOptions = function( o, cmdO ) { - # o and this.opts() seem to be the same (command-specific options) + // 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) + // 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() ) + // 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 + // 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) -> + 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 + // Cache + EXTEND( true, _opts, o ); +}; -### Split multiple command-line filenames by the 'TO' keyword ### -splitSrcDest = () -> +/* Split multiple command-line filenames by the 'TO' keyword */ +var splitSrcDest = function() { - params = this.parent.args.filter((j) -> return String.is(j) ) - if params.length == 0 - #tmpName = @name() - throw { fluenterror: HMSTATUS.resumeNotFound, verb: @name(), quit: true } + 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 - splitAt = _.findIndex( params, (p) -> return p.toLowerCase() == 'to'; ) + // 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 + // 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 ) + chalk.yellow('.') ); + return; } + return { + src: params.slice(0, splitAt === -1 ? undefined : splitAt ), + dst: splitAt === -1 ? [] : params.slice( splitAt + 1 ) + }; +}; -### Simple logging placeholder. ### -logMsg = () -> - # eslint-disable-next-line no-console - _opts.silent || console.log.apply( console.log, arguments ) + +/* Simple logging placeholder. */ +var logMsg = function() { + // eslint-disable-next-line no-console + return _opts.silent || console.log.apply( console.log, arguments ); +}; diff --git a/src/cli/msg.js b/src/cli/msg.js index 1cf9021..d7588e6 100644 --- a/src/cli/msg.js +++ b/src/cli/msg.js @@ -1,10 +1,10 @@ -###* +/** 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' +const PATH = require('path'); +const YAML = require('yamljs'); +module.exports = YAML.load(PATH.join(__dirname, 'msg.yml')); diff --git a/src/cli/out.js b/src/cli/out.js index 8e6caed..df9194d 100644 --- a/src/cli/out.js +++ b/src/cli/out.js @@ -1,182 +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 -### +*/ -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'; +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 +/** A stateful output module. All HMR console output handled here. */ +class OutputHandler { - constructor: ( opts ) -> - @init opts - return + constructor( opts ) { + this.init(opts); + } - init: (opts) -> - @opts = EXTEND( true, @opts || { }, opts ) - @msgs = YAML.load(PATH.join( __dirname, 'msg.yml' )).events - return + init(opts) { + this.opts = EXTEND( true, this.opts || { }, opts ); + this.msgs = YAML.load(PATH.join( __dirname, 'msg.yml' )).events; + } - log: -> - printf = require('printf') - finished = printf.apply( printf, arguments ) - @opts.silent || console.log( finished ) # eslint-disable-line no-console + log() { + printf = require('printf'); + const finished = printf.apply( printf, arguments ); + return this.opts.silent || console.log( finished ); // eslint-disable-line no-console + } - do: ( evt ) -> + do( evt ) { - that = @ - L = () -> that.log.apply( that, arguments ) + const that = this; + const L = function() { return that.log.apply( that, arguments ); }; - switch evt.sub + switch (evt.sub) { - when HME.begin - this.opts.debug && - L( M2C( this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase() ) + 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; + //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 ) + case HME.afterCreate: + L( M2C( this.msgs.beforeCreate.msg, evt.isError ? 'red' : 'green' ), evt.fmt, evt.file ); break; - when HME.beforeTheme - this.opts.debug && - L( M2C( this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase() ) + case HME.beforeTheme: + return 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 ) + case HME.afterParse: + return 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') ) + 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') ); - 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'), + 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, if numFormats == 1 then '' else 's' ) + numFormats, numFormats === 1 ? '' : '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')) + 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; - 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 + 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; + } + } + } - L( M2C( this.msgs.afterGenerate.msg[2] + suffix, if evt.error then 'red' else 'green' ), + 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 ) ); - when HME.beforeAnalyze - L( M2C( this.msgs.beforeAnalyze.msg, 'green' ), evt.fmt, evt.file) + case HME.beforeAnalyze: + return 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) ) + 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) ); - when HME.beforeConvert - L( M2C( this.msgs.beforeConvert.msg, if evt.error then 'red' else 'green' ), + case HME.beforeConvert: + return L( M2C( this.msgs.beforeConvert.msg, evt.error ? 'red' : 'green' ), evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt ); - when HME.afterInlineConvert - L( M2C( this.msgs.afterInlineConvert.msg, 'gray', 'white.dim' ), + case HME.afterInlineConvert: + return 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) + 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, (err) -> + if (evt.violations) { + _.each(evt.violations, function(err) { L( chalk.yellow.bold('--> ') + chalk.yellow(err.field.replace('data.','resume.').toUpperCase() + - ' ' + err.message)) - return - , @ - return + ' ' + err.message)); + } + , this); + } + return; - when HME.afterPeek - sty = if evt.error then 'red' else ( if evt.target != undefined then 'green' else 'yellow' ) + 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) + // "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 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 - L M2C( this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file + // 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 - L chalk.red( evt.error.inner.inner ) + } else if (evt.error) { + return L(chalk.red( evt.error.inner.inner )); + } + break; + } + } +} -module.exports = OutputHandler +module.exports = OutputHandler; diff --git a/src/core/default-formats.js b/src/core/default-formats.js index 64d1fb5..4cbf0d6 100644 --- a/src/core/default-formats.js +++ b/src/core/default-formats.js @@ -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'))() } -] +]; diff --git a/src/core/default-options.js b/src/core/default-options.js index a84973f..2aade8f 100644 --- a/src/core/default-options.js +++ b/src/core/default-options.js @@ -1,13 +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 +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 diff --git a/src/core/event-codes.js b/src/core/event-codes.js index 420a0ed..1cd6a36 100644 --- a/src/core/event-codes.js +++ b/src/core/event-codes.js @@ -1,38 +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 +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 +}; diff --git a/src/core/fluent-date.js b/src/core/fluent-date.js index 92472cf..4ee22e2 100644 --- a/src/core/fluent-date.js +++ b/src/core/fluent-date.js @@ -1,15 +1,22 @@ -###* +/* + * 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 -### +*/ -moment = require 'moment' -require('../utils/string') +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 @@ -24,54 +31,65 @@ 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 +class FluentDate { - constructor: (dt) -> - @rep = this.fmt dt + constructor(dt) { + this.rep = this.fmt(dt); + } - @isCurrent: (dt) -> - !dt || (String.is(dt) and /^(present|now|current)$/.test(dt)) + static isCurrent(dt) { + return !dt || (String.is(dt) && /^(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 +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 = ( dt, throws ) -> +FluentDate.fmt = function( dt, throws ) { - throws = (throws == undefined || throws == null) || 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 + 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; + } +}; diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index 32826c5..8484f67 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -1,43 +1,50 @@ -###* +/* + * 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 -### +*/ -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' +const FS = require('fs'); +const extend = require('extend'); +let validator = require('is-my-json-valid'); +const _ = require('underscore'); +const __ = require('lodash'); +const PATH = require('path'); +const moment = require('moment'); +const XML = require('xml-escape'); +const MD = require('marked'); +const CONVERTER = require('fresh-jrs-converter'); +const JRSResume = require('./jrs-resume'); +const 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 +*/ +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 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 & @@ -49,390 +56,429 @@ class FreshResume# extends AbstractResume sort: Sort resume items by date. compute: Prepare computed resume totals. } - ### - parseJSON: ( rep, opts ) -> + */ + 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 + let scrubbed; + if (opts && opts.privatize) { + // Ignore any element with the 'ignore: true' or 'private: true' designator. + let ignoreList, privateList; + const 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 + // 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 !@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 = { + // 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 ) -> - @imp.file = filename || @imp.file - FS.writeFileSync @imp.file, @stringify(), 'utf8' - @ + /** 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 ) -> + */ + saveAs( filename, format ) { - # If format isn't specified, default to FRESH - safeFormat = (format && format.trim()) || 'FRESH' + // 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 + // Validate against the FRESH version regex + // freshVersionReg = require '../utils/fresh-version-regex' + // if (not freshVersionReg().test( safeFormat )) + // throw badVer: safeFormat - parts = safeFormat.split '@' + const parts = safeFormat.split('@'); - if parts[0] == 'FRESH' - @imp.file = filename || @imp.file - FS.writeFileSync @imp.file, @stringify(), 'utf8' + 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 = 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 - @ + } 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: () -> - jso = extend true, { }, @ - rnew = new FreshResume() - rnew.parseJSON jso, { } - rnew + */ + 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: () -> FreshResume.stringify @ + */ + 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 ) -> - ret = this.dupe() - trx = require '../utils/string-transformer' - trx ret, filt, transformer + */ + 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: () -> + */ + markdownify() { - MDIN = ( txt ) -> - return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, '') + const MDIN = txt => MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); - trx = ( key, val ) -> - if key == 'summary' - return MD val - MDIN(val) + const trx = function( key, val ) { + if (key === 'summary') { + return MD(val); + } + return MDIN(val); + }; - return @transformStrings ['skills','url','start','end','date'], trx + 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: () -> - trx = (key, val) -> XML val - return @transformStrings [], trx + */ + xmlify() { + const trx = (key, val) => XML(val); + return this.transformStrings([], trx); + } - ###* Return the resume format. ### - format: () -> 'FRESH' + /** Return the resume format. */ + format() { return 'FRESH'; } - ###* + /** Return internal metadata. Create if it doesn't exist. - ### - i: () -> this.imp = this.imp || { } + */ + 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. + // 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 + 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 - 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 + */ + 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 ) -> - return 0 if !obj - return obj.history.length if obj.history - return obj.sets.length if obj.sets - obj.length || 0; + */ + 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 ) -> - 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] ) + /** 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] ); - @[ moniker ] = @[ moniker ] || [] - if @[ moniker ].history - @[ moniker ].history.push newObject - else if moniker == 'skills' - @skills.sets.push newObject - else - @[ moniker ].push newObject - newObject + 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() - @social && _.some @social, (p) -> - p.network.trim().toLowerCase() == socialNetwork + */ + 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() - @social && _.find @social, (sn) -> - sn.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() - @social && _.filter @social, (sn) -> - sn.network.trim().toLowerCase() == socialNetwork + */ + 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() - @skills && _.some @skills, (sk) -> - sk.keywords && _.some sk.keywords, (kw) -> - kw.trim().toLowerCase() == skill + /** 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: ( info ) -> - schemaObj = require 'fresh-resume-schema' - validator = require 'is-my-json-valid' - validate = validator( schemaObj, { # See Note [1]. + /** Validate the sheet against the FRESH Resume schema. */ + isValid( info ) { + 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})?)?$/ } - }) - ret = validate @ - if !ret + }); + const ret = validate(this); + if (!ret) { this.imp = this.imp || { }; this.imp.validationErrors = validate.errors; - ret + } + return ret; + } - duration: (unit) -> - inspector = require '../inspectors/duration-inspector' - inspector.run @, 'employment.history', 'start', 'end', unit + 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: () -> + */ + 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 ) + 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; } } + }; - sortSection = ( key ) -> - ar = __.get this, key - if ar && ar.length - datedThings = obj.filter (o) -> o.start - datedThings.sort( byDateDesc ); + const sortSection = function( key ) { + const ar = __.get(this, key); + if (ar && ar.length) { + const datedThings = obj.filter(o => o.start); + return datedThings.sort( byDateDesc ); + } + }; - sortSection 'employment.history' - sortSection 'education.history' - sortSection 'service.history' - sortSection 'projects' + 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 + 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 +*/ +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 +*/ +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. -### -_parseDates = () -> +*/ +var _parseDates = function() { - _fmt = require('./fluent-date').fmt - that = @ + const _fmt = require('./fluent-date').fmt; + const that = this; - # TODO: refactor recursion - replaceDatesInObject = ( obj ) -> + // TODO: refactor recursion + var replaceDatesInObject = function( 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 + 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 +/** 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}$/ -# +// 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}$/ +// diff --git a/src/core/fresh-theme.js b/src/core/fresh-theme.js index 6e4432b..c2c6d59 100644 --- a/src/core/fresh-theme.js +++ b/src/core/fresh-theme.js @@ -1,231 +1,264 @@ -###* +/* + * 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. -### +*/ -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' +const FS = require('fs'); +const validator = require('is-my-json-valid'); +const _ = require('underscore'); +const PATH = require('path'); +const parsePath = require('parse-filepath'); +const pathExists = require('path-exists').sync; +const EXTEND = require('extend'); +const HMSTATUS = require('./status-codes'); +const moment = require('moment'); +const loadSafeJson = require('../utils/safe-json-loader'); +const READFILES = require('recursive-readdir-sync'); -### A representation of a FRESH theme asset. -@class FRESHTheme ### -class FRESHTheme +/* A representation of a FRESH theme asset. +@class FRESHTheme */ +class FRESHTheme { - constructor: () -> - @baseFolder = 'src' - return + constructor() { + this.baseFolder = 'src'; + } - ### Open and parse the specified theme. ### - open: ( themeFolder ) -> + /* Open and parse the specified theme. */ + open( themeFolder ) { - @folder = themeFolder + this.folder = themeFolder; - # Open the [theme-name].json file; should have the same name as folder - pathInfo = parsePath themeFolder + // Open the [theme-name].json file; should have the same name as folder + const pathInfo = parsePath(themeFolder); - # Set up a formats hash for the theme - formatsHash = { } + // Set up a formats hash for the theme + let formatsHash = { }; - # Load the theme - themeFile = PATH.join themeFolder, 'theme.json' - themeInfo = loadSafeJson themeFile - if themeInfo.ex - throw + // Load the theme + const themeFile = PATH.join(themeFolder, 'theme.json'); + const themeInfo = loadSafeJson(themeFile); + if (themeInfo.ex) { + throw{ fluenterror: - if themeInfo.ex.op == 'parse' - then HMSTATUS.parseError - else HMSTATUS.readError + themeInfo.ex.op === 'parse' + ? HMSTATUS.parseError + : HMSTATUS.readError, inner: themeInfo.ex.inner + }; + } - that = this + const that = this; - # Move properties from the theme JSON file to the theme object - EXTEND true, @, themeInfo.json + // 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 @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 + // 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 - d = parsePath( th ).dirname - themePath = PATH.join d, th + ); + } else { + const d = parsePath( th ).dirname; + themePath = PATH.join(d, th); + } - cached[ th ] = cached[th] || new FRESHTheme().open( themePath ) - formatsHash[ key ] = cached[ th ].getFormat( key ) + cached[ th ] = cached[th] || new FRESHTheme().open( themePath ); + return formatsHash[ key ] = cached[ th ].getFormat( key ); + }); + } - # Load theme files - formatsHash = _load.call @, formatsHash + // Load theme files + formatsHash = _load.call(this, formatsHash); - # Cache - @formats = formatsHash + // Cache + this.formats = formatsHash; - # Set the official theme name - @name = parsePath( @folder ).name - @ + // Set the official theme name + this.name = parsePath( this.folder ).name; + return this; + } - ### Determine if the theme supports the specified output format. ### - hasFormat: ( fmt ) -> _.has @formats, fmt + /* 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 ) -> @formats[ fmt ] + /* Determine if the theme supports the specified output format. */ + getFormat( fmt ) { return this.formats[ fmt ]; } +} -### Load and parse theme source files. ### -_load = (formatsHash) -> +/* Load and parse theme source files. */ +var _load = function(formatsHash) { - that = @ - major = false - tplFolder = PATH.join @folder, @baseFolder + const that = this; + const major = false; + const tplFolder = PATH.join(this.folder, this.baseFolder); - copyOnly = ['.ttf','.otf', '.png','.jpg','.jpeg','.pdf'] + const 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 - , @ + // 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... - @cssFiles = fmts.filter (fmt) -> fmt and (fmt.ext == 'css') + // 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. - @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 } + // 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. - jsFiles = fmts.filter (fmt) -> fmt and (fmt.ext == 'js') - @.jsFiles = jsFiles.map (jsf) -> jsf['path'] + // 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']); - formatsHash + return formatsHash; +}; -### Load a single theme file. ### -_loadOne = ( absPath, formatsHash, tplFolder ) -> +/* Load a single theme file. */ +var _loadOne = function( absPath, formatsHash, tplFolder ) { - pathInfo = parsePath absPath - return if pathInfo.basename.toLowerCase() == 'theme.json' + const pathInfo = parsePath(absPath); + if (pathInfo.basename.toLowerCase() === 'theme.json') { return; } - absPathSafe = absPath.trim().toLowerCase() - outFmt = '' - act = 'copy' - isPrimary = false + 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 @explicit + // 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( @formats ), ( fmtKey ) -> - fmtVal = @formats[ fmtKey ] - _.some fmtVal.transform, (fpath) -> - absPathB = PATH.join( @folder, fpath ).trim().toLowerCase() - absPathB == absPathSafe - , @ - , @ - act = 'transform' if outFmt + 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 - 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 + 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 - 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' + // 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 + // 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; } - # 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' + // 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 ) - obj + // 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. ### -friendlyName = ( val ) -> - val = (val && val.trim().toLowerCase()) || '' - friendly = { yml: 'yaml', md: 'markdown', txt: 'text' } - friendly[val] || val +/* 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 +module.exports = FRESHTheme; + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js index 23c6b02..fa65187 100644 --- a/src/core/jrs-resume.js +++ b/src/core/jrs-resume.js @@ -1,304 +1,352 @@ -###* +/* + * 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 -### +*/ -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') +const FS = require('fs'); +const extend = require('extend'); +let validator = require('is-my-json-valid'); +const _ = require('underscore'); +const PATH = require('path'); +const MD = require('marked'); +const CONVERTER = require('fresh-jrs-converter'); +const 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 +*/ +var JRSResume = (function() { + let clear = undefined; + 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) { + let ignoreList, privateList; + const 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); + + // 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; + } - ###* Initialize the the JSResume from string. ### - parse: ( stringData, opts ) -> - @imp = @imp ? raw: stringData - this.parseJSON JSON.parse( stringData ), opts + /** 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); + } - ###* - 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 + /** 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) + ); + } - # 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() - @ + /** 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; + } - ###* 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' - @ + duration(unit) { + const inspector = require('../inspectors/duration-inspector'); + return inspector.run(this, 'work', 'startDate', 'endDate', unit); + } - ###* 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' - @ + /** + 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; } + }; - ###* Return the resume format. ### - format: () -> 'JRS' + 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; } + }); + } - stringify: () -> JRSResume.stringify( @ ) + dupe() { + const rnew = new JRSResume(); + rnew.parse(this.stringify(), { }); + return rnew; + } - ###* 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 + /** + Create a copy of this resume in which all fields have been interpreted as + Markdown. + */ + harden() { - ###* - 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 ? { } + const ret = this.dupe(); + const HD = txt => `@@@@~${txt}~@@@@`; + const HDIN = txt => + //return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); + HD(txt) + ; - ###* 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 + 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(); + return JRSResume; +})(); - ###* 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 +/** Get the default (empty) sheet. */ +JRSResume.default = () => new JRSResume().parseJSON(require('fresh-resume-starter').jrs); - ###* 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>\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', +*/ +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 ) -> return key.trim() == val - return if temp then undefined else value - JSON.stringify obj, replacer, 2 + 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. -### -_parseDates = () -> +*/ +var _parseDates = function() { - _fmt = require('./fluent-date').fmt + const _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 ) + 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 function/ctor. -### -module.exports = JRSResume +*/ +module.exports = JRSResume; diff --git a/src/core/jrs-theme.js b/src/core/jrs-theme.js index 0d4d26d..af2320d 100644 --- a/src/core/jrs-theme.js +++ b/src/core/jrs-theme.js @@ -1,51 +1,56 @@ -###* +/* + * 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. -### +*/ -_ = require 'underscore' -PATH = require 'path' -parsePath = require 'parse-filepath' -pathExists = require('path-exists').sync -errors = require './status-codes' +const _ = require('underscore'); +const PATH = require('path'); +const parsePath = require('parse-filepath'); +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 +*/ +class JRSTheme { - ###* + /** Open and parse the specified JRS theme. @method open - ### - open: ( thFolder ) -> + */ + open( thFolder ) { - @folder = thFolder - pathInfo = parsePath thFolder + this.folder = thFolder; + const 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' + // 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' + // 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, @@ -53,8 +58,9 @@ class JRSTheme ext: 'html', css: null }] - pdf: - outFormat: 'pdf' + }, + pdf: { + outFormat: 'pdf', files: [{ action: 'transform', render: this.render, @@ -62,25 +68,30 @@ class JRSTheme ext: 'pdf', css: null }] - else - throw fluenterror: errors.missingPackageJSON - @ + } + }; + } else { + throw {fluenterror: errors.missingPackageJSON}; + } + return this; + } - ###* + /** Determine if the theme supports the output format. @method hasFormat - ### - hasFormat: ( fmt ) -> _.has this.formats, fmt + */ + hasFormat( fmt ) { return _.has(this.formats, fmt); } - ###* + /** Return the requested output format. @method getFormat - ### - getFormat: ( fmt ) -> @formats[ fmt ] + */ + getFormat( fmt ) { return this.formats[ fmt ]; } +} module.exports = JRSTheme; diff --git a/src/core/resume-factory.js b/src/core/resume-factory.js index 8854c28..f6cb699 100644 --- a/src/core/resume-factory.js +++ b/src/core/resume-factory.js @@ -1,33 +1,38 @@ -###* +/* + * 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 -### +*/ -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' +const FS = require('fs'); +const HMS = require('./status-codes'); +const HME = require('./event-codes'); +const ResumeConverter = require('fresh-jrs-converter'); +const chalk = require('chalk'); +const SyntaxErrorEx = require('../utils/syntax-error-ex'); +const _ = require('underscore'); +const resumeDetect = require('../utils/resume-detector'); +require('string.prototype.startswith'); -###* +/** A simple factory class for FRESH and JSON Resumes. @class ResumeFactory -### +*/ -ResumeFactory = module.exports = +const ResumeFactory = (module.exports = { - ###* + /** Load one or more resumes from disk. @param {Object} opts An options object with settings for the factory as well @@ -41,72 +46,85 @@ ResumeFactory = module.exports = } } - ### - load: ( sources, opts, emitter ) -> - sources.map( (src) -> - @loadOne( src, opts, emitter ) - , @) + */ + 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 ) -> + /** Load a single resume from disk. */ + loadOne( src, opts, emitter ) { - toFormat = opts.format # Can be null + let toFormat = opts.format; // Can be null - # Get the destination format. Can be 'fresh', 'jrs', or null/undefined. - toFormat && (toFormat = toFormat.toLowerCase().trim()) + // 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 + // Load and parse the resume JSON + const 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 + // 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 and ( orgFormat != toFormat ) - json = ResumeConverter[ 'to' + toFormat.toUpperCase() ] json + // 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 + // 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; + } - file: src - json: info.json - rez: rez + return { + file: src, + json: info.json, + rez + }; + } +}); -_parse = ( fileName, opts, eve ) -> +var _parse = function( fileName, opts, eve ) { - rawData = null - try + let rawData = null; + try { - # Read the file + // 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' + // 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. - fluenterror: if rawData then HMS.parseError else HMS.readError - inner: err - raw: rawData - file: fileName + 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 + }; + } +}; diff --git a/src/core/status-codes.js b/src/core/status-codes.js index 4a34cc9..e464924 100644 --- a/src/core/status-codes.js +++ b/src/core/status-codes.js @@ -1,40 +1,41 @@ -###* +/** 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 +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 +}; diff --git a/src/generators/base-generator.js b/src/generators/base-generator.js index 351f9ee..d271dde 100644 --- a/src/generators/base-generator.js +++ b/src/generators/base-generator.js @@ -1,22 +1,37 @@ -###* +/* + * decaffeinate suggestions: + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +/** Definition of the BaseGenerator class. @module generators/base-generator @license MIT. See LICENSE.md for details. -### +*/ -###* +/** The BaseGenerator class is the root of the generator hierarchy. Functionality common to ALL generators lives here. -### +*/ -module.exports = class BaseGenerator +let BaseGenerator; +module.exports = (BaseGenerator = (function() { + BaseGenerator = class BaseGenerator { + static initClass() { + + /** Status codes. */ + this.prototype.codes = require('../core/status-codes'); + + /** Generator options. */ + this.prototype.opts = { }; + } - ###* Base-class initialize. ### - constructor: ( @format ) -> - - ###* Status codes. ### - codes: require '../core/status-codes' - - ###* Generator options. ### - opts: { } + /** Base-class initialize. */ + constructor( format ) { + this.format = format; + } + }; + BaseGenerator.initClass(); + return BaseGenerator; +})()); diff --git a/src/generators/html-generator.js b/src/generators/html-generator.js index df2f144..0aca5e7 100644 --- a/src/generators/html-generator.js +++ b/src/generators/html-generator.js @@ -1,30 +1,39 @@ -###* +/* + * 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 HTMLGenerator class. @module generators/html-generator @license MIT. See LICENSE.md for details. -### +*/ -TemplateGenerator = require './template-generator' -FS = require 'fs-extra' -HTML = require 'html' -PATH = require 'path' -require 'string.prototype.endswith' +let HtmlGenerator; +const TemplateGenerator = require('./template-generator'); +const FS = require('fs-extra'); +const HTML = require('html'); +const PATH = require('path'); +require('string.prototype.endswith'); -module.exports = class HtmlGenerator extends TemplateGenerator +module.exports = (HtmlGenerator = class HtmlGenerator extends TemplateGenerator { - constructor: -> super 'html' + 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 @opts.prettify - then HTML.prettyPrint info.mk, this.opts.prettify - else info.mk + */ + 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; } + } +}); diff --git a/src/generators/html-pdf-cli-generator.js b/src/generators/html-pdf-cli-generator.js index f7682c8..3ac6be4 100644 --- a/src/generators/html-pdf-cli-generator.js +++ b/src/generators/html-pdf-cli-generator.js @@ -1,112 +1,130 @@ -###* +/* + * decaffeinate suggestions: + * DS103: Rewrite code to no longer use __guard__ + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +/** Definition of the HtmlPdfCLIGenerator class. @module generators/html-pdf-generator.js @license MIT. See LICENSE.md for details. -### +*/ -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' +let HtmlPdfCLIGenerator; +const TemplateGenerator = require('./template-generator'); +const FS = require('fs-extra'); +const PATH = require('path'); +const SLASH = require('slash'); +const _ = require('underscore'); +const HMSTATUS = require('../core/status-codes'); +const 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 = class HtmlPdfCLIGenerator extends TemplateGenerator +module.exports = (HtmlPdfCLIGenerator = class HtmlPdfCLIGenerator extends TemplateGenerator { - constructor: () -> super 'pdf', 'html' + constructor() { super('pdf', 'html'); } - ###* Generate the binary PDF. ### - onBeforeSave: ( info ) -> - #console.dir _.omit( info, 'mk' ), depth: null, colors: true - return info.mk if info.ext != 'html' and info.ext != 'pdf' - safe_eng = info.opts.pdf || 'wkhtmltopdf' - safe_eng = 'phantomjs' if safe_eng == 'phantom' - if _.has engines, safe_eng - @errHandler = info.opts.errHandler - engines[ safe_eng ].call @, info.mk, info.outputFile, info.opts, @onError - return null # halt further processing + /** Generate the binary PDF. */ + onBeforeSave( info ) { + //console.dir _.omit( info, 'mk' ), depth: null, colors: true + if ((info.ext !== 'html') && (info.ext !== 'pdf')) { return info.mk; } + let 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 + /* 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) -> - param.errHandler?.err? HMSTATUS.pdfGeneration, ex - return + spawn-watch here but that causes issues on legacy Node.js. */ + onError(ex, param) { + __guardMethod__(param.errHandler, 'err', o => o.err(HMSTATUS.pdfGeneration, ex)); + } +}); -# TODO: Move each engine to a separate module -engines = +// TODO: Move each engine to a separate module +var engines = { - ###* + /** Generate a PDF from HTML using wkhtmltopdf's CLI interface. Spawns a child process with `wkhtmltopdf `. 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: (markup, fOut, opts, on_error) -> - # Save the markup to a temporary file - tempFile = fOut.replace /\.pdf$/i, '.pdf.html' - FS.writeFileSync tempFile, markup, 'utf8' + */ + wkhtmltopdf(markup, fOut, opts, on_error) { + // Save the markup to a temporary file + const 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, (v, k) -> ['--' + k, v] - wkargs = wkopts.concat [ tempFile, fOut ] + // Prepare wkhtmltopdf arguments. + let wkopts = _.extend({'margin-top': '10mm', 'margin-bottom': '10mm'}, opts.wkhtmltopdf); + wkopts = _.flatten(_.map(wkopts, (v, k) => [`--${k}`, v])); + const wkargs = wkopts.concat([ tempFile, fOut ]); - SPAWN 'wkhtmltopdf', wkargs , false, on_error, @ - return + SPAWN('wkhtmltopdf', wkargs , false, on_error, this); + }, - ###* + /** Generate a PDF from HTML using Phantom's CLI interface. Spawns a child process with `phantomjs