From 0f65e4c9f30e32d3a1f3538232a34c13d5d72041 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Fri, 29 Jan 2016 15:23:57 -0500 Subject: [PATCH] Finish HackMyCore reshaping. Reintroduce HackMyCore, dropping the interim submodule, and reorganize and improve tests. --- .gitmodules | 3 - Gruntfile.js | 10 +- dist/cli/error.js | 8 +- {src => dist/cli}/index.js | 4 +- dist/cli/main.js | 11 +- dist/cli/out.js | 8 +- dist/{hmc/dist => }/core/default-formats.js | 0 dist/{hmc/dist => }/core/default-options.js | 0 dist/{hmc/dist => }/core/empty-jrs.json | 0 dist/{hmc/dist => }/core/event-codes.js | 0 dist/{hmc/dist => }/core/fluent-date.js | 0 dist/{hmc/dist => }/core/fresh-resume.js | 12 +- dist/{hmc/dist => }/core/fresh-theme.js | 0 dist/{hmc/dist => }/core/jrs-resume.js | 61 +- dist/{hmc/dist => }/core/jrs-theme.js | 0 dist/{hmc/dist => }/core/resume-factory.js | 0 dist/{hmc/dist => }/core/resume.json | 0 dist/{hmc/dist => }/core/status-codes.js | 0 .../dist => }/generators/base-generator.js | 0 .../dist => }/generators/html-generator.js | 0 .../generators/html-pdf-cli-generator.js | 0 .../generators/html-png-generator.js | 0 .../dist => }/generators/json-generator.js | 0 .../generators/json-yaml-generator.js | 0 .../dist => }/generators/latex-generator.js | 0 .../generators/markdown-generator.js | 0 .../generators/template-generator.js | 0 .../dist => }/generators/text-generator.js | 0 .../dist => }/generators/word-generator.js | 0 .../dist => }/generators/xml-generator.js | 0 .../dist => }/generators/yaml-generator.js | 0 .../{hmc/dist => }/helpers/console-helpers.js | 0 .../{hmc/dist => }/helpers/generic-helpers.js | 0 .../dist => }/helpers/handlebars-helpers.js | 0 .../dist => }/helpers/underscore-helpers.js | 0 dist/hmc/dist/index.js | 49 -- dist/hmc/dist/utils/safe-spawn.js | 34 -- dist/hmc/package.json | 92 --- dist/index.js | 57 +- .../dist => }/inspectors/gap-inspector.js | 0 .../dist => }/inspectors/keyword-inspector.js | 0 .../dist => }/inspectors/totals-inspector.js | 0 .../renderers/handlebars-generator.js | 0 .../{hmc/dist => }/renderers/jrs-generator.js | 0 .../renderers/underscore-generator.js | 0 dist/{hmc/dist => }/utils/class.js | 0 dist/{hmc/dist => }/utils/file-contains.js | 0 dist/{hmc/dist => }/utils/html-to-wpml.js | 0 dist/{hmc/dist => }/utils/md2chalk.js | 0 dist/{hmc/dist => }/utils/rasterize.js | 0 dist/{hmc/dist => }/utils/safe-json-loader.js | 0 dist/utils/safe-spawn.js | 46 ++ .../dist => }/utils/string-transformer.js | 0 dist/{hmc/dist => }/utils/string.js | 0 dist/{hmc/dist => }/utils/syntax-error-ex.js | 0 dist/{hmc/dist => }/verbs/analyze.js | 0 dist/{hmc/dist => }/verbs/build.js | 0 dist/{hmc/dist => }/verbs/convert.js | 0 dist/{hmc/dist => }/verbs/create.js | 0 dist/{hmc/dist => }/verbs/peek.js | 4 +- dist/{hmc/dist => }/verbs/validate.js | 74 +-- dist/{hmc/dist => }/verbs/verb.js | 0 package.json | 3 +- src/cli/error.coffee | 26 +- src/cli/index.js | 22 + src/cli/main.coffee | 12 +- src/cli/out.coffee | 8 +- src/core/default-formats.coffee | 18 + src/core/default-options.coffee | 13 + src/core/empty-jrs.json | 77 +++ src/core/event-codes.coffee | 35 ++ src/core/fluent-date.coffee | 83 +++ src/core/fresh-resume.coffee | 425 ++++++++++++++ src/core/fresh-theme.coffee | 277 +++++++++ src/core/jrs-resume.coffee | 360 ++++++++++++ src/core/jrs-theme.coffee | 87 +++ src/core/resume-factory.coffee | 115 ++++ src/core/resume.json | 380 +++++++++++++ src/core/status-codes.coffee | 33 ++ src/generators/base-generator.coffee | 28 + src/generators/html-generator.coffee | 30 + src/generators/html-pdf-cli-generator.coffee | 91 +++ src/generators/html-png-generator.coffee | 52 ++ src/generators/json-generator.coffee | 35 ++ src/generators/json-yaml-generator.coffee | 30 + src/generators/latex-generator.coffee | 14 + src/generators/markdown-generator.coffee | 14 + src/generators/template-generator.coffee | 204 +++++++ src/generators/text-generator.coffee | 14 + src/generators/word-generator.coffee | 11 + src/generators/xml-generator.coffee | 13 + src/generators/yaml-generator.coffee | 15 + src/helpers/console-helpers.coffee | 51 ++ src/helpers/generic-helpers.coffee | 524 ++++++++++++++++++ src/helpers/handlebars-helpers.coffee | 20 + src/helpers/underscore-helpers.coffee | 24 + src/hmc | 1 - src/index.coffee | 44 ++ src/inspectors/gap-inspector.coffee | 139 +++++ src/inspectors/keyword-inspector.coffee | 68 +++ src/inspectors/totals-inspector.coffee | 36 ++ src/renderers/handlebars-generator.coffee | 95 ++++ src/renderers/jrs-generator.coffee | 44 ++ src/renderers/underscore-generator.coffee | 52 ++ src/utils/class.js | 72 +++ src/utils/file-contains.coffee | 7 + src/utils/html-to-wpml.coffee | 49 ++ src/utils/md2chalk.coffee | 15 + src/utils/rasterize.coffee | 58 ++ src/utils/safe-json-loader.coffee | 26 + src/utils/safe-spawn.coffee | 28 + src/utils/string-transformer.coffee | 43 ++ src/utils/string.coffee | 15 + src/utils/syntax-error-ex.coffee | 25 + src/verbs/analyze.coffee | 75 +++ src/verbs/build.coffee | 340 ++++++++++++ src/verbs/convert.coffee | 77 +++ src/verbs/create.coffee | 46 ++ src/verbs/peek.coffee | 67 +++ src/verbs/validate.coffee | 92 +++ src/verbs/verb.coffee | 71 +++ test/all.js | 11 + test/{ => scripts}/hmr-options.json | 0 test/scripts/test-api.js | 206 +++++++ test/{ => scripts}/test-cli.js | 29 +- test/scripts/test-fresh-sheet.js | 71 +++ test/{ => scripts}/test-hmr.txt | 0 test/scripts/test-jrs-sheet.js | 69 +++ .../test-output.js} | 47 +- test/scripts/test-themes.js | 86 +++ 130 files changed, 5384 insertions(+), 337 deletions(-) delete mode 100644 .gitmodules rename {src => dist/cli}/index.js (66%) rename dist/{hmc/dist => }/core/default-formats.js (100%) rename dist/{hmc/dist => }/core/default-options.js (100%) rename dist/{hmc/dist => }/core/empty-jrs.json (100%) rename dist/{hmc/dist => }/core/event-codes.js (100%) rename dist/{hmc/dist => }/core/fluent-date.js (100%) rename dist/{hmc/dist => }/core/fresh-resume.js (97%) rename dist/{hmc/dist => }/core/fresh-theme.js (100%) rename dist/{hmc/dist => }/core/jrs-resume.js (90%) rename dist/{hmc/dist => }/core/jrs-theme.js (100%) rename dist/{hmc/dist => }/core/resume-factory.js (100%) rename dist/{hmc/dist => }/core/resume.json (100%) rename dist/{hmc/dist => }/core/status-codes.js (100%) rename dist/{hmc/dist => }/generators/base-generator.js (100%) rename dist/{hmc/dist => }/generators/html-generator.js (100%) rename dist/{hmc/dist => }/generators/html-pdf-cli-generator.js (100%) rename dist/{hmc/dist => }/generators/html-png-generator.js (100%) rename dist/{hmc/dist => }/generators/json-generator.js (100%) rename dist/{hmc/dist => }/generators/json-yaml-generator.js (100%) rename dist/{hmc/dist => }/generators/latex-generator.js (100%) rename dist/{hmc/dist => }/generators/markdown-generator.js (100%) rename dist/{hmc/dist => }/generators/template-generator.js (100%) rename dist/{hmc/dist => }/generators/text-generator.js (100%) rename dist/{hmc/dist => }/generators/word-generator.js (100%) rename dist/{hmc/dist => }/generators/xml-generator.js (100%) rename dist/{hmc/dist => }/generators/yaml-generator.js (100%) rename dist/{hmc/dist => }/helpers/console-helpers.js (100%) rename dist/{hmc/dist => }/helpers/generic-helpers.js (100%) rename dist/{hmc/dist => }/helpers/handlebars-helpers.js (100%) rename dist/{hmc/dist => }/helpers/underscore-helpers.js (100%) delete mode 100644 dist/hmc/dist/index.js delete mode 100644 dist/hmc/dist/utils/safe-spawn.js delete mode 100644 dist/hmc/package.json rename dist/{hmc/dist => }/inspectors/gap-inspector.js (100%) rename dist/{hmc/dist => }/inspectors/keyword-inspector.js (100%) rename dist/{hmc/dist => }/inspectors/totals-inspector.js (100%) rename dist/{hmc/dist => }/renderers/handlebars-generator.js (100%) rename dist/{hmc/dist => }/renderers/jrs-generator.js (100%) rename dist/{hmc/dist => }/renderers/underscore-generator.js (100%) rename dist/{hmc/dist => }/utils/class.js (100%) rename dist/{hmc/dist => }/utils/file-contains.js (100%) rename dist/{hmc/dist => }/utils/html-to-wpml.js (100%) rename dist/{hmc/dist => }/utils/md2chalk.js (100%) rename dist/{hmc/dist => }/utils/rasterize.js (100%) rename dist/{hmc/dist => }/utils/safe-json-loader.js (100%) create mode 100644 dist/utils/safe-spawn.js rename dist/{hmc/dist => }/utils/string-transformer.js (100%) rename dist/{hmc/dist => }/utils/string.js (100%) rename dist/{hmc/dist => }/utils/syntax-error-ex.js (100%) rename dist/{hmc/dist => }/verbs/analyze.js (100%) rename dist/{hmc/dist => }/verbs/build.js (100%) rename dist/{hmc/dist => }/verbs/convert.js (100%) rename dist/{hmc/dist => }/verbs/create.js (100%) rename dist/{hmc/dist => }/verbs/peek.js (96%) rename dist/{hmc/dist => }/verbs/validate.js (55%) rename dist/{hmc/dist => }/verbs/verb.js (100%) create mode 100644 src/cli/index.js create mode 100644 src/core/default-formats.coffee create mode 100644 src/core/default-options.coffee create mode 100644 src/core/empty-jrs.json create mode 100644 src/core/event-codes.coffee create mode 100644 src/core/fluent-date.coffee create mode 100644 src/core/fresh-resume.coffee create mode 100644 src/core/fresh-theme.coffee create mode 100644 src/core/jrs-resume.coffee create mode 100644 src/core/jrs-theme.coffee create mode 100644 src/core/resume-factory.coffee create mode 100644 src/core/resume.json create mode 100644 src/core/status-codes.coffee create mode 100644 src/generators/base-generator.coffee create mode 100644 src/generators/html-generator.coffee create mode 100644 src/generators/html-pdf-cli-generator.coffee create mode 100644 src/generators/html-png-generator.coffee create mode 100644 src/generators/json-generator.coffee create mode 100644 src/generators/json-yaml-generator.coffee create mode 100644 src/generators/latex-generator.coffee create mode 100644 src/generators/markdown-generator.coffee create mode 100644 src/generators/template-generator.coffee create mode 100644 src/generators/text-generator.coffee create mode 100644 src/generators/word-generator.coffee create mode 100644 src/generators/xml-generator.coffee create mode 100644 src/generators/yaml-generator.coffee create mode 100644 src/helpers/console-helpers.coffee create mode 100644 src/helpers/generic-helpers.coffee create mode 100644 src/helpers/handlebars-helpers.coffee create mode 100644 src/helpers/underscore-helpers.coffee delete mode 160000 src/hmc create mode 100644 src/index.coffee create mode 100644 src/inspectors/gap-inspector.coffee create mode 100644 src/inspectors/keyword-inspector.coffee create mode 100644 src/inspectors/totals-inspector.coffee create mode 100644 src/renderers/handlebars-generator.coffee create mode 100644 src/renderers/jrs-generator.coffee create mode 100644 src/renderers/underscore-generator.coffee create mode 100644 src/utils/class.js create mode 100644 src/utils/file-contains.coffee create mode 100644 src/utils/html-to-wpml.coffee create mode 100644 src/utils/md2chalk.coffee create mode 100644 src/utils/rasterize.coffee create mode 100644 src/utils/safe-json-loader.coffee create mode 100644 src/utils/safe-spawn.coffee create mode 100644 src/utils/string-transformer.coffee create mode 100644 src/utils/string.coffee create mode 100644 src/utils/syntax-error-ex.coffee create mode 100644 src/verbs/analyze.coffee create mode 100644 src/verbs/build.coffee create mode 100644 src/verbs/convert.coffee create mode 100644 src/verbs/create.coffee create mode 100644 src/verbs/peek.coffee create mode 100644 src/verbs/validate.coffee create mode 100644 src/verbs/verb.coffee create mode 100644 test/all.js rename test/{ => scripts}/hmr-options.json (100%) create mode 100644 test/scripts/test-api.js rename test/{ => scripts}/test-cli.js (72%) create mode 100644 test/scripts/test-fresh-sheet.js rename test/{ => scripts}/test-hmr.txt (100%) create mode 100644 test/scripts/test-jrs-sheet.js rename test/{test-stdout.js => scripts/test-output.js} (80%) create mode 100644 test/scripts/test-themes.js diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index dd8b53e..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/hmc"] - path = src/hmc - url = git://github.com/hacksalot/HackMyCore.git diff --git a/Gruntfile.js b/Gruntfile.js index 01d3a9c..eedafdb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -10,13 +10,7 @@ module.exports = function (grunt) { main: { expand: true, cwd: 'src', - src: ['**/*','!**/*.coffee','!hmc/**'], - dest: 'dist/', - }, - core: { - expand: true, - cwd: 'src', - src: ['hmc/dist/**/*','hmc/package.json'], + src: ['**/*','!**/*.coffee'], dest: 'dist/', } }, @@ -25,7 +19,7 @@ module.exports = function (grunt) { main: { expand: true, cwd: 'src', - src: ['cli/**/*.coffee'], + src: ['**/*.coffee'], dest: 'dist/', ext: '.js' } diff --git a/dist/cli/error.js b/dist/cli/error.js index 2d6ca97..ab1f2ba 100644 --- a/dist/cli/error.js +++ b/dist/cli/error.js @@ -8,19 +8,19 @@ Error-handling routines for HackMyResume. (function() { var ErrorHandler, FCMD, FS, HMSTATUS, M2C, PATH, PKG, SyntaxErrorEx, WRAP, YAML, _defaultLog, assembleError, chalk, extend, printf; - HMSTATUS = require('../hmc/dist/core/status-codes'); + HMSTATUS = require('../core/status-codes'); PKG = require('../../package.json'); FS = require('fs'); - FCMD = require('../hmc'); + FCMD = require('../index'); PATH = require('path'); WRAP = require('word-wrap'); - M2C = require('../hmc/dist/utils/md2chalk.js'); + M2C = require('../utils/md2chalk'); chalk = require('chalk'); @@ -30,7 +30,7 @@ Error-handling routines for HackMyResume. printf = require('printf'); - SyntaxErrorEx = require('../hmc/dist/utils/syntax-error-ex'); + SyntaxErrorEx = require('../utils/syntax-error-ex'); require('string.prototype.startswith'); diff --git a/src/index.js b/dist/cli/index.js similarity index 66% rename from src/index.js rename to dist/cli/index.js index 8b40823..b6ce89f 100644 --- a/src/index.js +++ b/dist/cli/index.js @@ -12,11 +12,11 @@ Command-line interface (CLI) for HackMyResume. try { - require('./cli/main')( process.argv ); + require('./main')( process.argv ); } catch( ex ) { - require('./cli/error').err( ex, true ); + require('./error').err( ex, true ); } diff --git a/dist/cli/main.js b/dist/cli/main.js index a3347fb..cf71dc5 100644 --- a/dist/cli/main.js +++ b/dist/cli/main.js @@ -8,7 +8,7 @@ Definition of the `main` function. (function() { var Command, EXTEND, FS, HME, HMR, HMSTATUS, OUTPUT, PAD, PATH, PKG, StringUtils, _, _opts, _out, _title, chalk, execute, initOptions, initialize, loadOptions, logMsg, main, safeLoadJSON, splitSrcDest; - HMR = require('../hmc'); + HMR = require('../index'); PKG = require('../../package.json'); @@ -20,13 +20,13 @@ Definition of the `main` function. PATH = require('path'); - HMSTATUS = require('../hmc/dist/core/status-codes'); + HMSTATUS = require('../core/status-codes'); - HME = require('../hmc/dist/core/event-codes'); + HME = require('../core/event-codes'); - safeLoadJSON = require('../hmc/dist/utils/safe-json-loader'); + safeLoadJSON = require('../utils/safe-json-loader'); - StringUtils = require('../hmc/dist/utils/string.js'); + StringUtils = require('../utils/string.js'); _ = require('underscore'); @@ -210,6 +210,7 @@ Definition of the `main` function. }); v.invoke.call(v, src, dst, _opts, log); if (v.errorCode) { + console.log('Exiting with error code ' + v.errorCode); return process.exit(v.errorCode); } }; diff --git a/dist/cli/out.js b/dist/cli/out.js index ccc3093..db3dd6a 100644 --- a/dist/cli/out.js +++ b/dist/cli/out.js @@ -10,13 +10,13 @@ Output routines for HackMyResume. chalk = require('chalk'); - HME = require('../hmc/dist/core/event-codes'); + HME = require('../core/event-codes'); _ = require('underscore'); - Class = require('../hmc/dist/utils/class.js'); + Class = require('../utils/class.js'); - M2C = require('../hmc/dist/utils/md2chalk.js'); + M2C = require('../utils/md2chalk.js'); PATH = require('path'); @@ -110,7 +110,7 @@ Output routines for HackMyResume. case HME.afterAnalyze: info = evt.info; rawTpl = FS.readFileSync(PATH.join(__dirname, 'analyze.hbs'), 'utf8'); - HANDLEBARS.registerHelper(require('../hmc/dist/helpers/console-helpers')); + HANDLEBARS.registerHelper(require('../helpers/console-helpers')); template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false diff --git a/dist/hmc/dist/core/default-formats.js b/dist/core/default-formats.js similarity index 100% rename from dist/hmc/dist/core/default-formats.js rename to dist/core/default-formats.js diff --git a/dist/hmc/dist/core/default-options.js b/dist/core/default-options.js similarity index 100% rename from dist/hmc/dist/core/default-options.js rename to dist/core/default-options.js diff --git a/dist/hmc/dist/core/empty-jrs.json b/dist/core/empty-jrs.json similarity index 100% rename from dist/hmc/dist/core/empty-jrs.json rename to dist/core/empty-jrs.json diff --git a/dist/hmc/dist/core/event-codes.js b/dist/core/event-codes.js similarity index 100% rename from dist/hmc/dist/core/event-codes.js rename to dist/core/event-codes.js diff --git a/dist/hmc/dist/core/fluent-date.js b/dist/core/fluent-date.js similarity index 100% rename from dist/hmc/dist/core/fluent-date.js rename to dist/core/fluent-date.js diff --git a/dist/hmc/dist/core/fresh-resume.js b/dist/core/fresh-resume.js similarity index 97% rename from dist/hmc/dist/core/fresh-resume.js rename to dist/core/fresh-resume.js index 1a7713f..f9b8887 100644 --- a/dist/hmc/dist/core/fresh-resume.js +++ b/dist/core/fresh-resume.js @@ -55,6 +55,10 @@ Definition of the FRESHResume class. /** Initialize the the FreshResume from JSON string data. */ FreshResume.prototype.parse = function(stringData, opts) { + var ref; + this.imp = (ref = this.imp) != null ? ref : { + raw: stringData + }; return this.parseJSON(JSON.parse(stringData), opts); }; @@ -74,7 +78,7 @@ Definition of the FRESHResume class. */ FreshResume.prototype.parseJSON = function(rep, opts) { - var ignoreList, scrubbed, that, traverse; + var ignoreList, ref, scrubbed, that, traverse; that = this; traverse = require('traverse'); ignoreList = []; @@ -87,12 +91,16 @@ Definition of the FRESHResume class. } }); extend(true, this, scrubbed); - if (!this.imp) { + if (!((ref = this.imp) != null ? ref.processed : void 0)) { opts = opts || {}; if (opts.imp === void 0 || opts.imp) { this.imp = this.imp || {}; this.imp.title = (opts.title || this.imp.title) || this.name; + if (!this.imp.raw) { + this.imp.raw = JSON.stringify(rep); + } } + this.imp.processed = true; (opts.date === void 0 || opts.date) && _parseDates.call(this); (opts.sort === void 0 || opts.sort) && this.sort(); (opts.compute === void 0 || opts.compute) && (this.computed = { diff --git a/dist/hmc/dist/core/fresh-theme.js b/dist/core/fresh-theme.js similarity index 100% rename from dist/hmc/dist/core/fresh-theme.js rename to dist/core/fresh-theme.js diff --git a/dist/hmc/dist/core/jrs-resume.js b/dist/core/jrs-resume.js similarity index 90% rename from dist/hmc/dist/core/jrs-resume.js rename to dist/core/jrs-resume.js index c58b9ff..d2a1dfc 100644 --- a/dist/hmc/dist/core/jrs-resume.js +++ b/dist/core/jrs-resume.js @@ -39,24 +39,23 @@ Definition of the JRSResume class. /** Initialize the JSResume from file. */ - JRSResume.prototype.open = function(file, title) { - this.basics = { - imp: { - file: file, - raw: FS.readFileSync(file, 'utf8') - } - }; - return this.parse(this.basics.imp.raw, title); + JRSResume.prototype.open = function(file, opts) { + var raw, ret; + raw = FS.readFileSync(file, 'utf8'); + ret = this.parse(raw, opts); + this.imp.file = file; + return ret; }; /** Initialize the the JSResume from string. */ JRSResume.prototype.parse = function(stringData, opts) { - var rep; - opts = opts || {}; - rep = JSON.parse(stringData); - return this.parseJSON(rep, opts); + var ref; + this.imp = (ref = this.imp) != null ? ref : { + raw: stringData + }; + return this.parseJSON(JSON.parse(stringData), opts); }; @@ -75,7 +74,7 @@ Definition of the JRSResume class. */ JRSResume.prototype.parseJSON = function(rep, opts) { - var ignoreList, scrubbed, that, traverse; + var ignoreList, ref, scrubbed, that, traverse; opts = opts || {}; that = this; traverse = require('traverse'); @@ -89,10 +88,16 @@ Definition of the JRSResume class. } }); extend(true, this, scrubbed); - if (opts.imp === void 0 || opts.imp) { - this.basics.imp = this.basics.imp || {}; - this.basics.imp.title = (opts.title || this.basics.imp.title) || this.basics.name; - this.basics.imp.orgFormat = 'JRS'; + if (!((ref = this.imp) != null ? ref.processed : void 0)) { + opts = opts || {}; + if (opts.imp === void 0 || opts.imp) { + this.imp = this.imp || {}; + this.imp.title = (opts.title || this.imp.title) || this.basics.name; + if (!this.imp.raw) { + this.imp.raw = JSON.stringify(rep); + } + } + this.imp.processed = true; } (opts.date === void 0 || opts.date) && _parseDates.call(this); (opts.sort === void 0 || opts.sort) && this.sort(); @@ -109,8 +114,8 @@ Definition of the JRSResume class. /** Save the sheet to disk (for environments that have disk access). */ JRSResume.prototype.save = function(filename) { - this.basics.imp.file = filename || this.basics.imp.file; - FS.writeFileSync(this.basics.imp.file, this.stringify(this), 'utf8'); + this.imp.file = filename || this.imp.file; + FS.writeFileSync(this.imp.file, this.stringify(this), 'utf8'); return this; }; @@ -120,8 +125,8 @@ Definition of the JRSResume class. JRSResume.prototype.saveAs = function(filename, format) { var newRep, stringRep; if (format === 'JRS') { - this.basics.imp.file = filename || this.basics.imp.file; - FS.writeFileSync(this.basics.imp.file, this.stringify(), 'utf8'); + this.imp.file = filename || this.imp.file; + FS.writeFileSync(this.imp.file, this.stringify(), 'utf8'); } else { newRep = CONVERTER.toFRESH(this); stringRep = CONVERTER.toSTRING(newRep); @@ -163,9 +168,8 @@ Definition of the JRSResume class. */ JRSResume.prototype.i = function() { - this.basics = this.basics || {}; - this.basics.imp = this.basics.imp || {}; - return this.basics.imp; + var ref; + return this.imp = (ref = this.imp) != null ? ref : {}; }; @@ -225,7 +229,7 @@ Definition of the JRSResume class. /** Validate the sheet against the JSON Resume schema. */ JRSResume.prototype.isValid = function() { - var ret, schema, schemaObj, validate; + var ret, schema, schemaObj, temp, validate; schema = FS.readFileSync(PATH.join(__dirname, 'resume.json'), 'utf8'); schemaObj = JSON.parse(schema); validator = require('is-my-json-valid'); @@ -234,10 +238,13 @@ Definition of the JRSResume class. date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } }); + temp = this.imp; + delete this.imp; ret = validate(this); + this.imp = temp; if (!ret) { - this.basics.imp = this.basics.imp || {}; - this.basics.imp.validationErrors = validate.errors; + this.imp = this.imp || {}; + this.imp.validationErrors = validate.errors; } return ret; }; diff --git a/dist/hmc/dist/core/jrs-theme.js b/dist/core/jrs-theme.js similarity index 100% rename from dist/hmc/dist/core/jrs-theme.js rename to dist/core/jrs-theme.js diff --git a/dist/hmc/dist/core/resume-factory.js b/dist/core/resume-factory.js similarity index 100% rename from dist/hmc/dist/core/resume-factory.js rename to dist/core/resume-factory.js diff --git a/dist/hmc/dist/core/resume.json b/dist/core/resume.json similarity index 100% rename from dist/hmc/dist/core/resume.json rename to dist/core/resume.json diff --git a/dist/hmc/dist/core/status-codes.js b/dist/core/status-codes.js similarity index 100% rename from dist/hmc/dist/core/status-codes.js rename to dist/core/status-codes.js diff --git a/dist/hmc/dist/generators/base-generator.js b/dist/generators/base-generator.js similarity index 100% rename from dist/hmc/dist/generators/base-generator.js rename to dist/generators/base-generator.js diff --git a/dist/hmc/dist/generators/html-generator.js b/dist/generators/html-generator.js similarity index 100% rename from dist/hmc/dist/generators/html-generator.js rename to dist/generators/html-generator.js diff --git a/dist/hmc/dist/generators/html-pdf-cli-generator.js b/dist/generators/html-pdf-cli-generator.js similarity index 100% rename from dist/hmc/dist/generators/html-pdf-cli-generator.js rename to dist/generators/html-pdf-cli-generator.js diff --git a/dist/hmc/dist/generators/html-png-generator.js b/dist/generators/html-png-generator.js similarity index 100% rename from dist/hmc/dist/generators/html-png-generator.js rename to dist/generators/html-png-generator.js diff --git a/dist/hmc/dist/generators/json-generator.js b/dist/generators/json-generator.js similarity index 100% rename from dist/hmc/dist/generators/json-generator.js rename to dist/generators/json-generator.js diff --git a/dist/hmc/dist/generators/json-yaml-generator.js b/dist/generators/json-yaml-generator.js similarity index 100% rename from dist/hmc/dist/generators/json-yaml-generator.js rename to dist/generators/json-yaml-generator.js diff --git a/dist/hmc/dist/generators/latex-generator.js b/dist/generators/latex-generator.js similarity index 100% rename from dist/hmc/dist/generators/latex-generator.js rename to dist/generators/latex-generator.js diff --git a/dist/hmc/dist/generators/markdown-generator.js b/dist/generators/markdown-generator.js similarity index 100% rename from dist/hmc/dist/generators/markdown-generator.js rename to dist/generators/markdown-generator.js diff --git a/dist/hmc/dist/generators/template-generator.js b/dist/generators/template-generator.js similarity index 100% rename from dist/hmc/dist/generators/template-generator.js rename to dist/generators/template-generator.js diff --git a/dist/hmc/dist/generators/text-generator.js b/dist/generators/text-generator.js similarity index 100% rename from dist/hmc/dist/generators/text-generator.js rename to dist/generators/text-generator.js diff --git a/dist/hmc/dist/generators/word-generator.js b/dist/generators/word-generator.js similarity index 100% rename from dist/hmc/dist/generators/word-generator.js rename to dist/generators/word-generator.js diff --git a/dist/hmc/dist/generators/xml-generator.js b/dist/generators/xml-generator.js similarity index 100% rename from dist/hmc/dist/generators/xml-generator.js rename to dist/generators/xml-generator.js diff --git a/dist/hmc/dist/generators/yaml-generator.js b/dist/generators/yaml-generator.js similarity index 100% rename from dist/hmc/dist/generators/yaml-generator.js rename to dist/generators/yaml-generator.js diff --git a/dist/hmc/dist/helpers/console-helpers.js b/dist/helpers/console-helpers.js similarity index 100% rename from dist/hmc/dist/helpers/console-helpers.js rename to dist/helpers/console-helpers.js diff --git a/dist/hmc/dist/helpers/generic-helpers.js b/dist/helpers/generic-helpers.js similarity index 100% rename from dist/hmc/dist/helpers/generic-helpers.js rename to dist/helpers/generic-helpers.js diff --git a/dist/hmc/dist/helpers/handlebars-helpers.js b/dist/helpers/handlebars-helpers.js similarity index 100% rename from dist/hmc/dist/helpers/handlebars-helpers.js rename to dist/helpers/handlebars-helpers.js diff --git a/dist/hmc/dist/helpers/underscore-helpers.js b/dist/helpers/underscore-helpers.js similarity index 100% rename from dist/hmc/dist/helpers/underscore-helpers.js rename to dist/helpers/underscore-helpers.js diff --git a/dist/hmc/dist/index.js b/dist/hmc/dist/index.js deleted file mode 100644 index cf5e62e..0000000 --- a/dist/hmc/dist/index.js +++ /dev/null @@ -1,49 +0,0 @@ - -/** -External API surface for HackMyResume. -@license MIT. See LICENSE.md for details. -@module hackmycore/index - */ - - -/** -API facade for HackMyCore. - */ - -(function() { - var HackMyCore; - - HackMyCore = module.exports = { - verbs: { - build: require('./verbs/build'), - analyze: require('./verbs/analyze'), - validate: require('./verbs/validate'), - convert: require('./verbs/convert'), - "new": require('./verbs/create'), - peek: require('./verbs/peek') - }, - alias: { - generate: require('./verbs/build'), - create: require('./verbs/create') - }, - options: require('./core/default-options'), - formats: require('./core/default-formats'), - Sheet: require('./core/fresh-resume'), - FRESHResume: require('./core/fresh-resume'), - JRSResume: require('./core/jrs-resume'), - FRESHTheme: require('./core/fresh-theme'), - JRSTheme: require('./core/jrs-theme'), - FluentDate: require('./core/fluent-date'), - HtmlGenerator: require('./generators/html-generator'), - TextGenerator: require('./generators/text-generator'), - HtmlPdfCliGenerator: require('./generators/html-pdf-cli-generator'), - WordGenerator: require('./generators/word-generator'), - MarkdownGenerator: require('./generators/markdown-generator'), - JsonGenerator: require('./generators/json-generator'), - YamlGenerator: require('./generators/yaml-generator'), - JsonYamlGenerator: require('./generators/json-yaml-generator'), - LaTeXGenerator: require('./generators/latex-generator'), - HtmlPngGenerator: require('./generators/html-png-generator') - }; - -}).call(this); diff --git a/dist/hmc/dist/utils/safe-spawn.js b/dist/hmc/dist/utils/safe-spawn.js deleted file mode 100644 index 330ed8a..0000000 --- a/dist/hmc/dist/utils/safe-spawn.js +++ /dev/null @@ -1,34 +0,0 @@ - -/** -Safe spawn utility for HackMyResume / FluentCV. -@module utils/safe-spawn -@license MIT. See LICENSE.md for details. - */ - -(function() { - module.exports = function(cmd, args, isSync) { - var info, spawn; - try { - spawn = require('child_process')[isSync ? 'spawnSync' : 'spawn']; - info = spawn(cmd, args); - if (!isSync) { - return info.on('error', function(err) { - throw { - cmd: 'wkhtmltopdf', - inner: err - }; - }); - } else { - if (info.error) { - throw { - cmd: 'wkhtmltopdf', - inner: info.error - }; - } - } - } catch (_error) { - throw _error; - } - }; - -}).call(this); diff --git a/dist/hmc/package.json b/dist/hmc/package.json deleted file mode 100644 index 2bd3691..0000000 --- a/dist/hmc/package.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "name": "hackmycore", - "version": "0.4.0", - "description": "The open core library for HackMyResume and FluentCV.", - "repository": { - "type": "git", - "url": "https://github.com/hacksalot/HackMyCore.git" - }, - "scripts": { - "test": "grunt clean:test && mocha", - "grunt": "grunt" - }, - "author": "hacksalot (https://github.com/hacksalot)", - "contributors": [], - "license": "MIT", - "bugs": { - "url": "https://github.com/hacksalot/HackMyCore/issues" - }, - "main": "dist/index.js", - "homepage": "https://github.com/hacksalot/HackMyCore", - "dependencies": { - "copy": "^0.1.3", - "extend": "^3.0.0", - "fresca": "~0.6.0", - "fresh-jrs-converter": "^0.2.0", - "fresh-resume-starter": "^0.2.2", - "fresh-themes": "~0.14.0-beta", - "fs-extra": "^0.24.0", - "handlebars": "^4.0.5", - "html": "0.0.10", - "is-my-json-valid": "^2.12.2", - "json-lint": "^0.1.0", - "jst": "0.0.13", - "lodash": "^3.10.1", - "marked": "^0.3.5", - "mkdirp": "^0.5.1", - "moment": "^2.10.6", - "parse-filepath": "^0.6.3", - "path-exists": "^2.1.0", - "printf": "^0.2.3", - "recursive-readdir-sync": "^1.0.6", - "simple-html-tokenizer": "^0.2.0", - "slash": "^1.0.0", - "string-padding": "^1.0.2", - "string.prototype.endswith": "^0.2.0", - "string.prototype.startswith": "^0.2.0", - "traverse": "^0.6.6", - "underscore": "^1.8.3", - "word-wrap": "^1.1.0", - "xml-escape": "^1.0.0", - "yamljs": "^0.2.4" - }, - "devDependencies": { - "chai": "*", - "chalk": "^1.1.1", - "fresh-test-resumes": "^0.6.0", - "grunt": "*", - "grunt-cli": "^0.1.13", - "grunt-contrib-clean": "^0.7.0", - "grunt-contrib-coffee": "^0.13.0", - "grunt-contrib-copy": "^0.8.2", - "grunt-contrib-jshint": "^0.11.3", - "grunt-contrib-yuidoc": "^0.10.0", - "grunt-jsdoc": "^1.1.0", - "grunt-simple-mocha": "*", - "jsonresume-theme-boilerplate": "^0.1.2", - "jsonresume-theme-classy": "^1.0.9", - "jsonresume-theme-modern": "0.0.18", - "jsonresume-theme-sceptile": "^1.0.5", - "mocha": "*", - "resample": "fluentdesk/resample" - }, - "keywords": [ - "resume", - "CV", - "portfolio", - "employment", - "career", - "HackMyResume", - "Markdown", - "JSON", - "Word", - "PDF", - "YAML", - "HTML", - "LaTeX", - "CLI", - "Handlebars", - "Underscore", - "template" - ] -} diff --git a/dist/index.js b/dist/index.js index 8b40823..cf5e62e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,22 +1,49 @@ -#! /usr/bin/env node +/** +External API surface for HackMyResume. +@license MIT. See LICENSE.md for details. +@module hackmycore/index + */ /** -Command-line interface (CLI) for HackMyResume. -@license MIT. See LICENSE.md for details. -@module index.js -*/ +API facade for HackMyCore. + */ +(function() { + var HackMyCore; + HackMyCore = module.exports = { + verbs: { + build: require('./verbs/build'), + analyze: require('./verbs/analyze'), + validate: require('./verbs/validate'), + convert: require('./verbs/convert'), + "new": require('./verbs/create'), + peek: require('./verbs/peek') + }, + alias: { + generate: require('./verbs/build'), + create: require('./verbs/create') + }, + options: require('./core/default-options'), + formats: require('./core/default-formats'), + Sheet: require('./core/fresh-resume'), + FRESHResume: require('./core/fresh-resume'), + JRSResume: require('./core/jrs-resume'), + FRESHTheme: require('./core/fresh-theme'), + JRSTheme: require('./core/jrs-theme'), + FluentDate: require('./core/fluent-date'), + HtmlGenerator: require('./generators/html-generator'), + TextGenerator: require('./generators/text-generator'), + HtmlPdfCliGenerator: require('./generators/html-pdf-cli-generator'), + WordGenerator: require('./generators/word-generator'), + MarkdownGenerator: require('./generators/markdown-generator'), + JsonGenerator: require('./generators/json-generator'), + YamlGenerator: require('./generators/yaml-generator'), + JsonYamlGenerator: require('./generators/json-yaml-generator'), + LaTeXGenerator: require('./generators/latex-generator'), + HtmlPngGenerator: require('./generators/html-png-generator') + }; -try { - - require('./cli/main')( process.argv ); - -} -catch( ex ) { - - require('./cli/error').err( ex, true ); - -} +}).call(this); diff --git a/dist/hmc/dist/inspectors/gap-inspector.js b/dist/inspectors/gap-inspector.js similarity index 100% rename from dist/hmc/dist/inspectors/gap-inspector.js rename to dist/inspectors/gap-inspector.js diff --git a/dist/hmc/dist/inspectors/keyword-inspector.js b/dist/inspectors/keyword-inspector.js similarity index 100% rename from dist/hmc/dist/inspectors/keyword-inspector.js rename to dist/inspectors/keyword-inspector.js diff --git a/dist/hmc/dist/inspectors/totals-inspector.js b/dist/inspectors/totals-inspector.js similarity index 100% rename from dist/hmc/dist/inspectors/totals-inspector.js rename to dist/inspectors/totals-inspector.js diff --git a/dist/hmc/dist/renderers/handlebars-generator.js b/dist/renderers/handlebars-generator.js similarity index 100% rename from dist/hmc/dist/renderers/handlebars-generator.js rename to dist/renderers/handlebars-generator.js diff --git a/dist/hmc/dist/renderers/jrs-generator.js b/dist/renderers/jrs-generator.js similarity index 100% rename from dist/hmc/dist/renderers/jrs-generator.js rename to dist/renderers/jrs-generator.js diff --git a/dist/hmc/dist/renderers/underscore-generator.js b/dist/renderers/underscore-generator.js similarity index 100% rename from dist/hmc/dist/renderers/underscore-generator.js rename to dist/renderers/underscore-generator.js diff --git a/dist/hmc/dist/utils/class.js b/dist/utils/class.js similarity index 100% rename from dist/hmc/dist/utils/class.js rename to dist/utils/class.js diff --git a/dist/hmc/dist/utils/file-contains.js b/dist/utils/file-contains.js similarity index 100% rename from dist/hmc/dist/utils/file-contains.js rename to dist/utils/file-contains.js diff --git a/dist/hmc/dist/utils/html-to-wpml.js b/dist/utils/html-to-wpml.js similarity index 100% rename from dist/hmc/dist/utils/html-to-wpml.js rename to dist/utils/html-to-wpml.js diff --git a/dist/hmc/dist/utils/md2chalk.js b/dist/utils/md2chalk.js similarity index 100% rename from dist/hmc/dist/utils/md2chalk.js rename to dist/utils/md2chalk.js diff --git a/dist/hmc/dist/utils/rasterize.js b/dist/utils/rasterize.js similarity index 100% rename from dist/hmc/dist/utils/rasterize.js rename to dist/utils/rasterize.js diff --git a/dist/hmc/dist/utils/safe-json-loader.js b/dist/utils/safe-json-loader.js similarity index 100% rename from dist/hmc/dist/utils/safe-json-loader.js rename to dist/utils/safe-json-loader.js diff --git a/dist/utils/safe-spawn.js b/dist/utils/safe-spawn.js new file mode 100644 index 0000000..dda5e9d --- /dev/null +++ b/dist/utils/safe-spawn.js @@ -0,0 +1,46 @@ + +/** +Safe spawn utility for HackMyResume / FluentCV. +@module utils/safe-spawn +@license MIT. See LICENSE.md for details. + */ + +(function() { + module.exports = function(cmd, args, isSync, callback) { + var info, spawn; + try { + spawn = require('child_process')[isSync ? 'spawnSync' : 'spawn']; + info = spawn(cmd, args); + if (!isSync) { + info.on('error', function(err) { + if (callback != null) { + callback(err); + } else { + throw { + cmd: cmd, + inner: err + }; + } + }); + } else { + if (info.error) { + if (callback != null) { + callback(err); + } else { + throw { + cmd: cmd, + inner: info.error + }; + } + } + } + } catch (_error) { + if (callback != null) { + return callback(_error); + } else { + throw _error; + } + } + }; + +}).call(this); diff --git a/dist/hmc/dist/utils/string-transformer.js b/dist/utils/string-transformer.js similarity index 100% rename from dist/hmc/dist/utils/string-transformer.js rename to dist/utils/string-transformer.js diff --git a/dist/hmc/dist/utils/string.js b/dist/utils/string.js similarity index 100% rename from dist/hmc/dist/utils/string.js rename to dist/utils/string.js diff --git a/dist/hmc/dist/utils/syntax-error-ex.js b/dist/utils/syntax-error-ex.js similarity index 100% rename from dist/hmc/dist/utils/syntax-error-ex.js rename to dist/utils/syntax-error-ex.js diff --git a/dist/hmc/dist/verbs/analyze.js b/dist/verbs/analyze.js similarity index 100% rename from dist/hmc/dist/verbs/analyze.js rename to dist/verbs/analyze.js diff --git a/dist/hmc/dist/verbs/build.js b/dist/verbs/build.js similarity index 100% rename from dist/hmc/dist/verbs/build.js rename to dist/verbs/build.js diff --git a/dist/hmc/dist/verbs/convert.js b/dist/verbs/convert.js similarity index 100% rename from dist/hmc/dist/verbs/convert.js rename to dist/verbs/convert.js diff --git a/dist/hmc/dist/verbs/create.js b/dist/verbs/create.js similarity index 100% rename from dist/hmc/dist/verbs/create.js rename to dist/verbs/create.js diff --git a/dist/hmc/dist/verbs/peek.js b/dist/verbs/peek.js similarity index 96% rename from dist/hmc/dist/verbs/peek.js rename to dist/verbs/peek.js index a78c109..d21eabd 100644 --- a/dist/hmc/dist/verbs/peek.js +++ b/dist/verbs/peek.js @@ -34,9 +34,7 @@ Implementation of the 'peek' verb for HackMyResume. }); - /** - Peek at a resume, resume section, or resume field. - */ + /** Peek at a resume, resume section, or resume field. */ peek = function(src, dst, opts) { var objPath; diff --git a/dist/hmc/dist/verbs/validate.js b/dist/verbs/validate.js similarity index 55% rename from dist/hmc/dist/verbs/validate.js rename to dist/verbs/validate.js index ef80fc6..66a5f37 100644 --- a/dist/hmc/dist/verbs/validate.js +++ b/dist/verbs/validate.js @@ -6,7 +6,7 @@ Implementation of the 'validate' verb for HackMyResume. */ (function() { - var FS, HMEVENT, HMSTATUS, ResumeFactory, SyntaxErrorEx, ValidateVerb, Verb, _, chalk, validate; + var FS, HMEVENT, HMSTATUS, ResumeFactory, SyntaxErrorEx, ValidateVerb, Verb, _, chalk, safeLoadJSON, validate; FS = require('fs'); @@ -24,6 +24,8 @@ Implementation of the 'validate' verb for HackMyResume. _ = require('underscore'); + safeLoadJSON = require('../utils/safe-json-loader'); + /** An invokable resume validation command. */ @@ -32,11 +34,13 @@ Implementation of the 'validate' verb for HackMyResume. return this._super('validate'); }, invoke: function() { + var ret; this.stat(HMEVENT.begin, { cmd: 'validate' }); - validate.apply(this, arguments); + ret = validate.apply(this, arguments); this.stat(HMEVENT.end); + return ret; } }); @@ -44,7 +48,7 @@ Implementation of the 'validate' verb for HackMyResume. /** Validate 1 to N resumes in FRESH or JSON Resume format. */ validate = function(sources, unused, opts) { - var resumes, schemas, validator; + var schemas, validator; if (!sources || !sources.length) { throw { fluenterror: HMSTATUS.resumeNotFoundAlt, @@ -56,53 +60,51 @@ Implementation of the 'validate' verb for HackMyResume. fresh: require('fresca'), jars: require('../core/resume.json') }; - resumes = ResumeFactory.load(sources, { - format: null, - objectify: false - }, this); - return resumes.map(function(src) { - var errors, exc, fmt, json, ret; + return _.map(sources, function(t) { + var errCode, errors, fmt, json, obj, ret; ret = { - file: src, + file: t, isValid: false }; - if (src.fluenterror) { - if (opts.assert) { - throw src; - } - this.setError(src.fluenterror, src); - return ret; - } - json = src.json; - fmt = json.basics ? 'jrs' : 'fresh'; - errors = []; - try { - validate = validator(schemas[fmt], { - formats: { - date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ + obj = safeLoadJSON(t); + if (!obj.ex) { + json = obj.json; + fmt = json.basics ? 'jrs' : 'fresh'; + errors = []; + try { + validate = validator(schemas[fmt], { + formats: { + date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ + } + }); + ret.isValid = validate(json); + if (!ret.isValid) { + errors = validate.errors; } - }); - ret.isValid = validate(json); - if (!ret.isValid) { - errors = validate.errors; + } catch (_error) { + ret.ex = _error; } - } catch (_error) { - exc = _error; - return ret; + } else { + errCode = obj.ex.operation === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError; + if (errCode === HMSTATUS.readError) { + obj.ex.quiet = true; + } + this.setError(errCode, obj.ex); + this.err(errCode, obj.ex); } this.stat(HMEVENT.afterValidate, { - file: src.file, + file: t, isValid: ret.isValid, - fmt: fmt.replace('jars', 'JSON Resume'), + fmt: fmt != null ? fmt.replace('jars', 'JSON Resume') : void 0, errors: errors }); if (opts.assert && !ret.isValid) { throw { - fluenterror: HMSTATUS.invalid({ - shouldExit: true - }) + fluenterror: HMSTATUS.invalid, + shouldExit: true }; } + console.log('1111'); return ret; }, this); }; diff --git a/dist/hmc/dist/verbs/verb.js b/dist/verbs/verb.js similarity index 100% rename from dist/hmc/dist/verbs/verb.js rename to dist/verbs/verb.js diff --git a/package.json b/package.json index 4249932..e5a77a9 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,9 @@ "url": "https://github.com/hacksalot/HackMyResume/issues" }, "bin": { - "hackmyresume": "dist/index.js" + "hackmyresume": "dist/cli/index.js" }, + "main": "src/index.js", "homepage": "https://github.com/hacksalot/HackMyResume", "dependencies": { "chalk": "^1.1.1", diff --git a/src/cli/error.coffee b/src/cli/error.coffee index b41a37a..e293170 100644 --- a/src/cli/error.coffee +++ b/src/cli/error.coffee @@ -6,19 +6,19 @@ Error-handling routines for HackMyResume. -HMSTATUS = require('../hmc/dist/core/status-codes') -PKG = require('../../package.json') -FS = require('fs') -FCMD = require('../hmc') -PATH = require('path') -WRAP = require('word-wrap') -M2C = require('../hmc/dist/utils/md2chalk.js') -chalk = require('chalk') -extend = require('extend') -YAML = require('yamljs') -printf = require('printf') -SyntaxErrorEx = require('../hmc/dist/utils/syntax-error-ex') -require('string.prototype.startswith') +HMSTATUS = require '../core/status-codes' +PKG = require '../../package.json' +FS = require 'fs' +FCMD = require '../index' +PATH = require 'path' +WRAP = require 'word-wrap' +M2C = require '../utils/md2chalk' +chalk = require 'chalk' +extend = require 'extend' +YAML = require 'yamljs' +printf = require 'printf' +SyntaxErrorEx = require '../utils/syntax-error-ex' +require 'string.prototype.startswith' diff --git a/src/cli/index.js b/src/cli/index.js new file mode 100644 index 0000000..b6ce89f --- /dev/null +++ b/src/cli/index.js @@ -0,0 +1,22 @@ +#! /usr/bin/env node + + + +/** +Command-line interface (CLI) for HackMyResume. +@license MIT. See LICENSE.md for details. +@module index.js +*/ + + + +try { + + require('./main')( process.argv ); + +} +catch( ex ) { + + require('./error').err( ex, true ); + +} diff --git a/src/cli/main.coffee b/src/cli/main.coffee index a4a3913..a6f3bc9 100644 --- a/src/cli/main.coffee +++ b/src/cli/main.coffee @@ -6,16 +6,16 @@ Definition of the `main` function. -HMR = require '../hmc' +HMR = require '../index' PKG = require '../../package.json' FS = require 'fs' EXTEND = require 'extend' chalk = require 'chalk' PATH = require 'path' -HMSTATUS = require '../hmc/dist/core/status-codes' -HME = require '../hmc/dist/core/event-codes' -safeLoadJSON = require '../hmc/dist/utils/safe-json-loader' -StringUtils = require '../hmc/dist/utils/string.js' +HMSTATUS = require '../core/status-codes' +HME = require '../core/event-codes' +safeLoadJSON = require '../utils/safe-json-loader' +StringUtils = require '../utils/string.js' _ = require 'underscore' OUTPUT = require './out' PAD = require 'string-padding' @@ -243,10 +243,12 @@ execute = ( src, dst, opts, log ) -> v.on( 'hmr:error', -> hand.err.apply( hand, arguments ) ) v.invoke.call( v, src, dst, _opts, log ) if v.errorCode + console.log 'Exiting with error code ' + v.errorCode process.exit(v.errorCode) + ### Initialize HackMyResume options. TODO: Options loading is a little hacky, for two reasons: diff --git a/src/cli/out.coffee b/src/cli/out.coffee index 8b9cf73..773b300 100644 --- a/src/cli/out.coffee +++ b/src/cli/out.coffee @@ -7,10 +7,10 @@ Output routines for HackMyResume. chalk = require('chalk') -HME = require('../hmc/dist/core/event-codes') +HME = require('../core/event-codes') _ = require('underscore') -Class = require('../hmc/dist/utils/class.js') -M2C = require('../hmc/dist/utils/md2chalk.js') +Class = require('../utils/class.js') +M2C = require('../utils/md2chalk.js') PATH = require('path') LO = require('lodash') FS = require('fs') @@ -109,7 +109,7 @@ OutputHandler = module.exports = Class.extend when HME.afterAnalyze info = evt.info rawTpl = FS.readFileSync( PATH.join( __dirname, 'analyze.hbs' ), 'utf8') - HANDLEBARS.registerHelper( require('../hmc/dist/helpers/console-helpers') ) + HANDLEBARS.registerHelper( require('../helpers/console-helpers') ) template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false }) tot = 0 info.keywords.forEach (g) -> tot += g.count diff --git a/src/core/default-formats.coffee b/src/core/default-formats.coffee new file mode 100644 index 0000000..64d1fb5 --- /dev/null +++ b/src/core/default-formats.coffee @@ -0,0 +1,18 @@ +### +Event code definitions. +@module core/default-formats +@license MIT. See LICENSE.md for details. +### + +###* Supported resume formats. ### +module.exports = [ + { name: 'html', ext: 'html', gen: new (require('../generators/html-generator'))() }, + { name: 'txt', ext: 'txt', gen: new (require('../generators/text-generator'))() }, + { name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../generators/word-generator'))() }, + { name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new (require('../generators/html-pdf-cli-generator'))() }, + { name: 'png', ext: 'png', fmt: 'html', is: false, gen: new (require('../generators/html-png-generator'))() }, + { name: 'md', ext: 'md', fmt: 'txt', gen: new (require('../generators/markdown-generator'))() }, + { name: 'json', ext: 'json', gen: new (require('../generators/json-generator'))() }, + { name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../generators/json-yaml-generator'))() }, + { name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../generators/latex-generator'))() } +] diff --git a/src/core/default-options.coffee b/src/core/default-options.coffee new file mode 100644 index 0000000..a84973f --- /dev/null +++ b/src/core/default-options.coffee @@ -0,0 +1,13 @@ +### +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 diff --git a/src/core/empty-jrs.json b/src/core/empty-jrs.json new file mode 100644 index 0000000..bedfe00 --- /dev/null +++ b/src/core/empty-jrs.json @@ -0,0 +1,77 @@ +{ + "basics": { + "name": "", + "label": "", + "picture": "", + "email": "", + "phone": "", + "degree": "", + "website": "", + "summary": "", + "location": { + "address": "", + "postalCode": "", + "city": "", + "countryCode": "", + "region": "" + }, + "profiles": [{ + "network": "", + "username": "", + "url": "" + }] + }, + + "work": [{ + "company": "", + "position": "", + "website": "", + "startDate": "", + "endDate": "", + "summary": "", + "highlights": [ + "" + ] + }], + + "awards": [{ + "title": "", + "date": "", + "awarder": "", + "summary": "" + }], + + "education": [{ + "institution": "", + "area": "", + "studyType": "", + "startDate": "", + "endDate": "", + "gpa": "", + "courses": [ "" ] + }], + + "publications": [{ + "name": "", + "publisher": "", + "releaseDate": "", + "website": "", + "summary": "" + }], + + "volunteer": [{ + "organization": "", + "position": "", + "website": "", + "startDate": "", + "endDate": "", + "summary": "", + "highlights": [ "" ] + }], + + "skills": [{ + "name": "", + "level": "", + "keywords": [""] + }] +} diff --git a/src/core/event-codes.coffee b/src/core/event-codes.coffee new file mode 100644 index 0000000..44c55a0 --- /dev/null +++ b/src/core/event-codes.coffee @@ -0,0 +1,35 @@ +### +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 diff --git a/src/core/fluent-date.coffee b/src/core/fluent-date.coffee new file mode 100644 index 0000000..94654fe --- /dev/null +++ b/src/core/fluent-date.coffee @@ -0,0 +1,83 @@ +###* +The HackMyResume date representation. +@license MIT. See LICENSE.md for details. +@module core/fluent-date +### + + + +moment = require 'moment' + +###* +Create a FluentDate from a string or Moment date object. There are a few date +formats to be aware of here. +1. The words "Present" and "Now", referring to the current date +2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10") +3. Year-and-month only ("2015-04") +4. Year-only "YYYY" ("2015") +5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008") +6. Empty dates ("", " ") +7. Any other date format that Moment.js can parse from +Note: Moment can transparently parse all or most of these, without requiring us +to specify a date format...but for maximum parsing safety and to avoid Moment +deprecation warnings, it's recommended to either a) explicitly specify the date +format or b) use an ISO format. For clarity, we handle these cases explicitly. +@class FluentDate +### + +class FluentDate + + constructor: (dt) -> + @rep = this.fmt dt + + +months = {} +abbr = {} +moment.months().forEach((m,idx) -> months[m.toLowerCase()] = idx+1 ) +moment.monthsShort().forEach((m,idx) -> abbr[m.toLowerCase()]=idx+1 ) +abbr.sept = 9 +module.exports = FluentDate + +FluentDate.fmt = ( dt, throws ) -> + + throws = (throws == undefined || throws == null) || throws + + if typeof dt == 'string' or dt instanceof String + dt = dt.toLowerCase().trim() + if /^(present|now|current)$/.test(dt) # "Present", "Now" + return moment() + else if /^\D+\s+\d{4}$/.test(dt) # "Mar 2015" + parts = dt.split(' '); + month = (months[parts[0]] || abbr[parts[0]]); + temp = parts[1] + '-' + (month < 10 ? '0' + month : month.toString()); + return moment temp, 'YYYY-MM' + else if /^\d{4}-\d{1,2}$/.test(dt) # "2015-03", "1998-4" + return moment dt, 'YYYY-MM' + else if /^\s*\d{4}\s*$/.test(dt) # "2015" + return moment dt, 'YYYY' + else if /^\s*$/.test(dt) # "", " " + defTime = + isNull: true + isBefore: ( other ) -> + if other and !other.isNull then true else false + isAfter: ( other ) -> + if other and !other.isNull then false else false + unix: () -> 0 + format: () -> '' + diff: () -> 0 + return defTime + 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 diff --git a/src/core/fresh-resume.coffee b/src/core/fresh-resume.coffee new file mode 100644 index 0000000..730c3d5 --- /dev/null +++ b/src/core/fresh-resume.coffee @@ -0,0 +1,425 @@ +###* +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' + + + +###* +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 + + ###* Initialize the FreshResume from file. ### + open: ( file, opts ) -> + raw = FS.readFileSync file, 'utf8' + ret = this.parse raw, opts + @imp.file = file + ret + + ###* Initialize the the FreshResume from JSON string data. ### + parse: ( stringData, opts ) -> + @imp = @imp ? raw: stringData + this.parseJSON JSON.parse( stringData ), opts + + + ###* + Initialize the FreshResume from JSON. + Open and parse the specified FRESH resume. Merge the JSON object model onto + this Sheet instance with extend() and convert sheet dates to a safe & + consistent format. Then sort each section by startDate descending. + @param rep {Object} The raw JSON representation. + @param opts {Object} Resume loading and parsing options. + { + date: Perform safe date conversion. + sort: Sort resume items by date. + compute: Prepare computed resume totals. + } + ### + parseJSON: ( rep, opts ) -> + + # Ignore any element with the 'ignore: true' designator. + that = @ + traverse = require 'traverse' + ignoreList = [] + scrubbed = traverse( rep ).map ( x ) -> + if !@isLeaf && @node.ignore + if @node.ignore == true || this.node.ignore == 'true' + ignoreList.push this.node + @remove() + + # Now apply the resume representation onto this object + extend( true, @, scrubbed ); + + # If the resume has already been processed, then we are being called from + # the .dupe method, and there's no need to do any post processing + if !@imp?.processed + # Set up metadata TODO: Clean up metadata on the object model. + opts = opts || { } + if opts.imp == undefined || opts.imp + @imp = @imp || { } + @imp.title = (opts.title || @imp.title) || @name + unless @imp.raw + @imp.raw = JSON.stringify rep + @imp.processed = true + # Parse dates, sort dates, and calculate computed values + (opts.date == undefined || opts.date) && _parseDates.call( this ); + (opts.sort == undefined || opts.sort) && this.sort(); + (opts.compute == undefined || opts.compute) && (@computed = { + numYears: this.duration(), + keywords: this.keywords() + }); + + @ + + + ###* Save the sheet to disk (for environments that have disk access). ### + save: ( filename ) -> + @imp.file = filename || @imp.file + FS.writeFileSync @imp.file, @stringify(), 'utf8' + @ + + + + ###* + Save the sheet to disk in a specific format, either FRESH or JSON Resume. + ### + saveAs: ( filename, format ) -> + if format != 'JRS' + @imp.file = filename || @imp.file + FS.writeFileSync @imp.file, @stringify(), 'utf8' + else + newRep = CONVERTER.toJRS this + FS.writeFileSync filename, JRSResume.stringify( newRep ), 'utf8' + @ + + + + + ###* + Duplicate this FreshResume instance. + This method first extend()s this object onto an empty, creating a deep copy, + and then passes the result into a new FreshResume instance via .parseJSON. + We do it this way to create a true clone of the object without re-running any + of the associated processing. + ### + dupe: () -> + jso = extend true, { }, @ + rnew = new FreshResume() + rnew.parseJSON jso, { } + rnew + + + + ###* + Convert this object to a JSON string, sanitizing meta-properties along the + way. + ### + stringify: () -> FreshResume.stringify @ + + + ###* + Create a copy of this resume in which all string fields have been run through + a transformation function (such as a Markdown filter or XML encoder). + TODO: Move this out of FRESHResume. + ### + transformStrings: ( filt, transformer ) -> + ret = this.dupe() + trx = require '../utils/string-transformer' + trx ret, filt, transformer + + + + ###* + Create a copy of this resume in which all fields have been interpreted as + Markdown. + ### + markdownify: () -> + + MDIN = ( txt ) -> + return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, '') + + trx = ( key, val ) -> + if key == 'summary' + return MD val + MDIN(val) + + return @transformStrings ['skills','url','start','end','date'], trx + + + + ###* + Create a copy of this resume in which all fields have been interpreted as + Markdown. + ### + xmlify: () -> + trx = (key, val) -> XML val + return @transformStrings [], trx + + + + ###* Return the resume format. ### + format: () -> 'FRESH' + + + + ###* + Return internal metadata. Create if it doesn't exist. + ### + i: () -> this.imp = this.imp || { } + + + + ###* Return a unique list of all keywords across all skills. ### + keywords: () -> + flatSkills = [] + if @skills + if @skills.sets + flatSkills = @skills.sets.map((sk) -> sk.skills ).reduce( (a,b) -> a.concat(b) ) + else if @skills.list + flatSkills = flatSkills.concat( this.skills.list.map (sk) -> return sk.name ) + flatSkills = _.uniq flatSkills + flatSkills + + + + ###* + Reset the sheet to an empty state. TODO: refactor/review + ### + clear: ( clearMeta ) -> + clearMeta = ((clearMeta == undefined) && true) || clearMeta + delete this.imp if clearMeta + delete this.computed # Don't use Object.keys() here + delete this.employment + delete this.service + delete this.education + delete this.recognition + delete this.reading + delete this.writing + delete this.interests + delete this.skills + delete this.social + + + + ###* + Get a safe count of the number of things in a section. + ### + count: ( obj ) -> + return 0 if !obj + return obj.history.length if obj.history + return obj.sets.length if obj.sets + obj.length || 0; + + + + + + + + ###* Add work experience to the sheet. ### + add: ( moniker ) -> + defSheet = FreshResume.default() + newObject = + if defSheet[moniker].history + then $.extend( true, {}, defSheet[ moniker ].history[0] ) + else + if moniker == 'skills' + then $.extend( true, {}, defSheet.skills.sets[0] ) + else $.extend( true, {}, defSheet[ moniker ][0] ) + + @[ moniker ] = @[ moniker ] || [] + if @[ moniker ].history + @[ moniker ].history.push newObject + else if moniker == 'skills' + @skills.sets.push newObject + else + @[ moniker ].push newObject + newObject + + + + + ###* + Determine if the sheet includes a specific social profile (eg, GitHub). + ### + hasProfile: ( socialNetwork ) -> + socialNetwork = socialNetwork.trim().toLowerCase() + @social && _.some @social, (p) -> + p.network.trim().toLowerCase() == socialNetwork + + + ###* Return the specified network profile. ### + getProfile: ( socialNetwork ) -> + socialNetwork = socialNetwork.trim().toLowerCase() + @social && _.find @social, (sn) -> + sn.network.trim().toLowerCase() == socialNetwork + + + ###* + Return an array of profiles for the specified network, for when the user + has multiple eg. GitHub accounts. + ### + getProfiles: ( socialNetwork ) -> + socialNetwork = socialNetwork.trim().toLowerCase() + @social && _.filter @social, (sn) -> + sn.network.trim().toLowerCase() == socialNetwork + + + ###* Determine if the sheet includes a specific skill. ### + hasSkill: ( skill ) -> + skill = skill.trim().toLowerCase() + @skills && _.some @skills, (sk) -> + sk.keywords && _.some sk.keywords, (kw) -> + kw.trim().toLowerCase() == skill + + + + ###* Validate the sheet against the FRESH Resume schema. ### + isValid: ( info ) -> + schemaObj = require 'fresca' + validator = require 'is-my-json-valid' + validate = validator( schemaObj, { # See Note [1]. + formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } + }) + ret = validate @ + if !ret + this.imp = this.imp || { }; + this.imp.validationErrors = validate.errors; + ret + + + + ###* + Calculate the total duration of the sheet. Assumes this.work has been sorted + by start date descending, perhaps via a call to Sheet.sort(). + @returns The total duration of the sheet's work history, that is, the number + of years between the start date of the earliest job on the resume and the + *latest end date of all jobs in the work history*. This last condition is for + sheets that have overlapping jobs. + ### + duration: (unit) -> + unit = unit || 'years' + empHist = __.get(this, 'employment.history') + if empHist && empHist.length + firstJob = _.last( empHist ) + careerStart = if firstJob.start then firstJob.safe.start else '' + if ((typeof careerStart == 'string' || careerStart instanceof String) && !careerStart.trim()) + return 0 + careerLast = _.max empHist, ( w ) -> + return if w.safe && w.safe.end then w.safe.end.unix() else moment().unix() + return careerLast.safe.end.diff careerStart, unit + 0 + + + + ###* + Sort dated things on the sheet by start date descending. Assumes that dates + on the sheet have been processed with _parseDates(). + ### + sort: () -> + + byDateDesc = (a,b) -> + if ( a.safe.start.isBefore(b.safe.start) ) + then 1 + else ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0 + + sortSection = ( key ) -> + ar = __.get this, key + if ar && ar.length + datedThings = obj.filter (o) -> o.start + datedThings.sort( byDateDesc ); + + sortSection 'employment.history' + sortSection 'education.history' + sortSection 'service.history' + sortSection 'projects' + + # this.awards && this.awards.sort( function(a, b) { + # return( a.safeDate.isBefore(b.safeDate) ) ? 1 + # : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0; + # }); + @writing && @writing.sort (a, b) -> + if a.safe.date.isBefore b.safe.date + then 1 + else ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0 + + +###* +Get the default (starter) sheet. +### +FreshResume.default = () -> + new FreshResume().parseJSON( require 'fresh-resume-starter' ) + + +###* +Convert the supplied FreshResume to a JSON string, sanitizing meta-properties +along the way. +### +FreshResume.stringify = ( obj ) -> + replacer = ( key,value ) -> # Exclude these keys from stringification + exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', + 'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'] + return if _.some( exKeys, (val) -> key.trim() == val ) + then undefined else value + JSON.stringify obj, replacer, 2 + + +###* +Convert human-friendly dates into formal Moment.js dates for all collections. +We don't want to lose the raw textual date as entered by the user, so we store +the Moment-ified date as a separate property with a prefix of .safe. For ex: +job.startDate is the date as entered by the user. job.safeStartDate is the +parsed Moment.js date that we actually use in processing. +### +_parseDates = () -> + + _fmt = require('./fluent-date').fmt + that = @ + + # TODO: refactor recursion + replaceDatesInObject = ( obj ) -> + + return if !obj + if Object.prototype.toString.call( obj ) == '[object Array]' + obj.forEach (elem) -> replaceDatesInObject( elem ) + 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' + + Object.keys( this ).forEach (member) -> replaceDatesInObject(that[member]) + + + +###* Export the Sheet function/ctor. ### +module.exports = FreshResume + +# Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats +# in addition to YYYY-MM-DD. The original regex: +# +# /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/ +# diff --git a/src/core/fresh-theme.coffee b/src/core/fresh-theme.coffee new file mode 100644 index 0000000..443321c --- /dev/null +++ b/src/core/fresh-theme.coffee @@ -0,0 +1,277 @@ +###* +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' + + + +### +The FRESHTheme class is a representation of a FRESH theme +asset. See also: JRSTheme. +@class FRESHTheme +### +class FRESHTheme + + + + + ### + Open and parse the specified theme. + ### + open: ( themeFolder ) -> + + this.folder = themeFolder; + + # Open the [theme-name].json file; should have the same name as folder + pathInfo = parsePath( themeFolder ) + + # Set up a formats hash for the theme + formatsHash = { } + + # Load the theme + themeFile = PATH.join( themeFolder, 'theme.json' ) + themeInfo = loadSafeJson( themeFile ) + if themeInfo.ex + throw + fluenterror: + if themeInfo.ex.operation == 'parse' + then HMSTATUS.parseError + else HMSTATUS.readError + inner: themeInfo.ex.inner + + that = this + + # Move properties from the theme JSON file to the theme object + EXTEND true, @, themeInfo.json + + # Check for an "inherits" entry in the theme JSON. + if @inherits + cached = { } + _.each @inherits, (th, key) -> + themesFolder = require.resolve 'fresh-themes' + d = parsePath( themeFolder ).dirname + themePath = PATH.join d, th + cached[ th ] = cached[th] || new FRESHTheme().open( themePath ) + formatsHash[ key ] = cached[ th ].getFormat( key ) + + # Check for an explicit "formats" entry in the theme JSON. If it has one, + # then this theme declares its files explicitly. + if !!@formats + formatsHash = loadExplicit.call this, formatsHash + @explicit = true; + else + formatsHash = loadImplicit.call this, formatsHash + + # Cache + @formats = formatsHash + + # Set the official theme name + @name = parsePath( @folder ).name + @ + + ### Determine if the theme supports the specified output format. ### + hasFormat: ( fmt ) -> _.has @formats, fmt + + ### Determine if the theme supports the specified output format. ### + getFormat: ( fmt ) -> @formats[ fmt ] + + +### Load the theme implicitly, by scanning the theme folder for files. TODO: +Refactor duplicated code with loadExplicit. ### +loadImplicit = (formatsHash) -> + + # Set up a hash of formats supported by this theme. + that = @ + major = false + + # Establish the base theme folder + tplFolder = PATH.join @folder, 'src' + + # Iterate over all files in the theme folder, producing an array, fmts, + # containing info for each file. While we're doing that, also build up + # the formatsHash object. + fmts = READFILES(tplFolder).map (absPath) -> + + # If this file lives in a specific format folder within the theme, + # such as "/latex" or "/html", then that format is the output format + # for all files within the folder. + pathInfo = parsePath absPath + outFmt = '' + isMajor = false + 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] + else + that.partials = that.partials || [] + that.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 ) + isMajor = true + + # We should have a valid output format now. + formatsHash[ outFmt ] = formatsHash[outFmt] || { + outFormat: outFmt, + files: [] + } + + # Create the file representation object. + obj = + action: 'transform' + path: absPath + major: isMajor + 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 + + # Now, get all the CSS files... + @cssFiles = fmts.filter (fmt) -> fmt and (fmt.ext == 'css') + + # For each CSS file, get its corresponding HTML file. It's possible that + # a theme can have a CSS file but *no* HTML file, as when a theme author + # creates a pure CSS override of an existing theme. + @cssFiles.forEach (cssf) -> + idx = _.findIndex fmts, ( fmt ) -> + fmt && fmt.pre == cssf.pre && fmt.ext == 'html' + cssf.major = false + if idx > -1 + fmts[ idx ].css = cssf.data + fmts[ idx ].cssPath = cssf.path + else + if that.inherits + # Found a CSS file without an HTML file in a theme that inherits + # from another theme. This is the override CSS file. + that.overrides = { file: cssf.path, data: cssf.data } + formatsHash + + + +### +Load the theme explicitly, by following the 'formats' hash +in the theme's JSON settings file. +### +loadExplicit = (formatsHash) -> + + # Housekeeping + tplFolder = PATH.join this.folder, 'src' + act = null + that = this + + # 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) -> + + act = null + # If this file is mentioned in the theme's JSON file under "transforms" + pathInfo = parsePath(absPath) + absPathSafe = absPath.trim().toLowerCase() + outFmt = _.find Object.keys( that.formats ), ( fmtKey ) -> + fmtVal = that.formats[ fmtKey ] + _.some fmtVal.transform, (fpath) -> + absPathB = PATH.join( that.folder, fpath ).trim().toLowerCase() + absPathB == absPathSafe + + 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 output format + # for all files within the folder. + if !outFmt + portion = pathInfo.dirname.replace tplFolder,'' + if portion && portion.trim() + reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig + res = reg.exec portion + res && (outFmt = res[1]) + + # Otherwise, the output format is inferred from the filename, as in + # compact-[outputformat].[extension], for ex, compact-pdf.html. + if !outFmt + idx = pathInfo.name.lastIndexOf '-' + outFmt = if (idx == -1) then pathInfo.name else pathInfo.name.substr(idx + 1) + + # We should have a valid output format now. + formatsHash[ outFmt ] = + formatsHash[ outFmt ] || { + outFormat: outFmt, + files: [], + symLinks: that.formats[ outFmt ].symLinks + }; + + # Create the file representation object. + obj = + action: act + orgPath: PATH.relative(that.folder, absPath) + path: 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 + + # Now, get all the CSS files... + @cssFiles = fmts.filter ( fmt ) -> fmt.ext == 'css' + + # For each CSS file, get its corresponding HTML file + @cssFiles.forEach ( cssf ) -> + # For each CSS file, get its corresponding HTML file + idx = _.findIndex fmts, ( fmt ) -> + fmt.pre == cssf.pre && fmt.ext == 'html' + fmts[ idx ].css = cssf.data; + fmts[ idx ].cssPath = cssf.path; + # Remove CSS files from the formats array + fmts = fmts.filter ( fmt) -> fmt.ext != 'css' + formatsHash + + + +### +Return a more friendly name for certain formats. +TODO: Refactor +### +friendlyName = ( val ) -> + val = val.trim().toLowerCase() + friendly = { yml: 'yaml', md: 'markdown', txt: 'text' } + friendly[val] || val + + +module.exports = FRESHTheme diff --git a/src/core/jrs-resume.coffee b/src/core/jrs-resume.coffee new file mode 100644 index 0000000..ba588c0 --- /dev/null +++ b/src/core/jrs-resume.coffee @@ -0,0 +1,360 @@ +###* +Definition of the JRSResume class. +@license MIT. See LICENSE.md for details. +@module core/jrs-resume +### + + + +FS = require('fs') +extend = require('extend') +validator = require('is-my-json-valid') +_ = require('underscore') +PATH = require('path') +MD = require('marked') +CONVERTER = require('fresh-jrs-converter') +moment = require('moment') + + + +###* +A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object +is an instantiation of that JSON decorated with utility methods. +@class JRSResume +### +class JRSResume + + + + ###* Initialize the JSResume from file. ### + open: ( file, opts ) -> + raw = FS.readFileSync file, 'utf8' + ret = this.parse raw, opts + @imp.file = file + ret + + + ###* Initialize the the JSResume from string. ### + parse: ( stringData, opts ) -> + @imp = @imp ? raw: stringData + this.parseJSON JSON.parse( stringData ), opts + + + + ###* + Initialize the JRSResume object from JSON. + Open and parse the specified JRS resume. Merge the JSON object model onto + this Sheet instance with extend() and convert sheet dates to a safe & + consistent format. Then sort each section by startDate descending. + @param rep {Object} The raw JSON representation. + @param opts {Object} Resume loading and parsing options. + { + date: Perform safe date conversion. + sort: Sort resume items by date. + compute: Prepare computed resume totals. + } + ### + parseJSON: ( rep, opts ) -> + opts = opts || { }; + + # Ignore any element with the 'ignore: true' designator. + that = this + traverse = require 'traverse' + ignoreList = [] + scrubbed = traverse( rep ).map ( x ) -> + if !@isLeaf && @node.ignore + if @node.ignore == true || this.node.ignore == 'true' + ignoreList.push @node + @remove() + + # Extend resume properties onto ourself. + extend true, this, scrubbed + + # Set up metadata + if !@imp?.processed + # Set up metadata TODO: Clean up metadata on the object model. + opts = opts || { } + if opts.imp == undefined || opts.imp + @imp = @imp || { } + @imp.title = (opts.title || @imp.title) || @basics.name + unless @imp.raw + @imp.raw = JSON.stringify rep + @imp.processed = true + # Parse dates, sort dates, and calculate computed values + (opts.date == undefined || opts.date) && _parseDates.call( this ) + (opts.sort == undefined || opts.sort) && this.sort() + if opts.compute == undefined || opts.compute + @basics.computed = + numYears: this.duration() + keywords: this.keywords() + @ + + + + ###* Save the sheet to disk (for environments that have disk access). ### + save: ( filename ) -> + @imp.file = filename || @imp.file + FS.writeFileSync @imp.file, @stringify( this ), 'utf8' + @ + + + + ###* Save the sheet to disk in a specific format, either FRESH or JRS. ### + saveAs: ( filename, format ) -> + if format == 'JRS' + @imp.file = filename || @imp.file; + FS.writeFileSync( @imp.file, @stringify(), 'utf8' ); + else + newRep = CONVERTER.toFRESH @ + stringRep = CONVERTER.toSTRING newRep + FS.writeFileSync filename, stringRep, 'utf8' + @ + + + + ###* Return the resume format. ### + format = () -> 'JRS' + + + + + stringify: () -> JRSResume.stringify( @ ) + + + + ###* Return a unique list of all keywords across all skills. ### + keywords: () -> + flatSkills = [] + if @skills && this.skills.length + @skills.forEach ( s ) -> flatSkills = _.union flatSkills, s.keywords + flatSkills + + + + ###* + Return internal metadata. Create if it doesn't exist. + JSON Resume v0.0.0 doesn't allow additional properties at the root level, + so tuck this into the .basic sub-object. + ### + i: () -> + @imp = @imp ? { } + + + + ###* Reset the sheet to an empty state. ### + clear = ( clearMeta ) -> + clearMeta = ((clearMeta == undefined) && true) || clearMeta; + delete this.imp if clearMeta + delete this.basics.computed # Don't use Object.keys() here + delete this.work + delete this.volunteer + delete this.education + delete this.awards + delete this.publications + delete this.interests + delete this.skills + delete this.basics.profiles + + + + + ###* Add work experience to the sheet. ### + add: ( moniker ) -> + defSheet = JRSResume.default() + newObject = $.extend( true, {}, defSheet[ moniker ][0] ) + this[ moniker ] = this[ moniker ] || [] + this[ moniker ].push( newObject ) + newObject + + + + ###* Determine if the sheet includes a specific social profile (eg, GitHub). ### + hasProfile: ( socialNetwork ) -> + socialNetwork = socialNetwork.trim().toLowerCase() + return @basics.profiles && _.some @basics.profiles, (p) -> + return p.network.trim().toLowerCase() == socialNetwork + + + + ###* Determine if the sheet includes a specific skill. ### + hasSkill: ( skill ) -> + skill = skill.trim().toLowerCase() + return this.skills && _.some this.skills, (sk) -> + return sk.keywords && _.some sk.keywords, (kw) -> + kw.trim().toLowerCase() == skill + + + + ###* Validate the sheet against the JSON Resume schema. ### + isValid: ( ) -> # TODO: ↓ fix this path ↓ + schema = FS.readFileSync PATH.join( __dirname, 'resume.json' ), 'utf8' + schemaObj = JSON.parse schema + validator = require 'is-my-json-valid' + validate = validator( schemaObj, { # Note [1] + formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } + }); + temp = @imp + delete @imp + ret = validate @ + @imp = temp + if !ret + @imp = @imp || { }; + @imp.validationErrors = validate.errors; + ret + + + + ###* + Calculate the total duration of the sheet. Assumes this.work has been sorted + by start date descending, perhaps via a call to Sheet.sort(). + @returns The total duration of the sheet's work history, that is, the number + of years between the start date of the earliest job on the resume and the + *latest end date of all jobs in the work history*. This last condition is for + sheets that have overlapping jobs. + ### + duration: ( unit ) -> + unit = unit || 'years'; + if this.work && this.work.length + careerStart = this.work[ this.work.length - 1].safeStartDate + if (typeof careerStart == 'string' || careerStart instanceof String) && !careerStart.trim() + return 0 + careerLast = _.max( this.work, ( w ) -> w.safeEndDate.unix() ).safeEndDate; + return careerLast.diff careerStart, unit + 0 + + + + ###* + 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: () -> + + that = @ + ret = @dupe() + + HD = (txt) -> '@@@@~' + txt + '~@@@@' + + HDIN = (txt) -> + #return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); + return HD txt + + # TODO: refactor recursion + hardenStringsInObject = ( obj, inline ) -> + + return if !obj + inline = inline == undefined || inline + + if Object.prototype.toString.call( obj ) == '[object Array]' + obj.forEach (elem, idx, ar) -> + if typeof elem == 'string' || elem instanceof String + ar[idx] = if inline then HDIN(elem) else HD( elem ) + else + hardenStringsInObject elem + else if typeof obj == 'object' + Object.keys( obj ).forEach (key) -> + sub = obj[key] + if typeof sub == 'string' || sub instanceof String + if _.contains(['skills','url','website','startDate','endDate', + 'releaseDate','date','phone','email','address','postalCode', + 'city','country','region'], key) + return + if key == 'summary' + obj[key] = HD( obj[key] ) + else + obj[key] = if inline then HDIN( obj[key] ) else HD( obj[key] ) + else + hardenStringsInObject sub + + + Object.keys( ret ).forEach (member) -> + hardenStringsInObject ret[ member ] + + ret + + + +###* Get the default (empty) sheet. ### +JRSResume.default = () -> + new JRSResume().open PATH.join( __dirname, 'empty-jrs.json'), 'Empty' + + + +###* +Convert this object to a JSON string, sanitizing meta-properties along the +way. Don't override .toString(). +### +JRSResume.stringify = ( obj ) -> + replacer = ( key,value ) -> # Exclude these keys from stringification + temp = _.some ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', + 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', + 'isModified', 'htmlPreview', 'display_progress_bar'], + ( val ) -> return key.trim() == val + return if temp then undefined else value + JSON.stringify obj, replacer, 2 + + +###* +Convert human-friendly dates into formal Moment.js dates for all collections. +We don't want to lose the raw textual date as entered by the user, so we store +the Moment-ified date as a separate property with a prefix of .safe. For ex: +job.startDate is the date as entered by the user. job.safeStartDate is the +parsed Moment.js date that we actually use in processing. +### +_parseDates = () -> + + _fmt = require('./fluent-date').fmt + + @work && @work.forEach (job) -> + job.safeStartDate = _fmt( job.startDate ) + job.safeEndDate = _fmt( job.endDate ) + @education && @education.forEach (edu) -> + edu.safeStartDate = _fmt( edu.startDate ) + edu.safeEndDate = _fmt( edu.endDate ) + @volunteer && @volunteer.forEach (vol) -> + vol.safeStartDate = _fmt( vol.startDate ) + vol.safeEndDate = _fmt( vol.endDate ) + @awards && @awards.forEach (awd) -> + awd.safeDate = _fmt( awd.date ) + @publications && @publications.forEach (pub) -> + pub.safeReleaseDate = _fmt( pub.releaseDate ) + + + +###* +Export the JRSResume function/ctor. +### +module.exports = JRSResume diff --git a/src/core/jrs-theme.coffee b/src/core/jrs-theme.coffee new file mode 100644 index 0000000..771e245 --- /dev/null +++ b/src/core/jrs-theme.coffee @@ -0,0 +1,87 @@ +###* +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 + + + +###* +The JRSTheme class is a representation of a JSON Resume theme asset. +@class JRSTheme +### +class JRSTheme + + +###* +Open and parse the specified theme. +@method open +### +open: ( thFolder ) -> + + @folder = thFolder + + # Open the [theme-name].json file; should have the same + # name as folder + pathInfo = parsePath thFolder + + # Open and parse the theme's package.json file. + pkgJsonPath = PATH.join thFolder, 'package.json' + if pathExists pkgJsonPath + thApi = require thFolder + thPkg = require pkgJsonPath + this.name = thPkg.name + this.render = (thApi && thApi.render) || undefined + this.engine = 'jrs' + + # Create theme formats (HTML and PDF). Just add the bare minimum mix of + # properties necessary to allow JSON Resume themes to share a rendering + # path with FRESH themes. + this.formats = + html: + outFormat: 'html' + files: [{ + action: 'transform', + render: this.render, + major: true, + ext: 'html', + css: null + }] + pdf: + outFormat: 'pdf' + files: [{ + action: 'transform', + render: this.render, + major: true, + ext: 'pdf', + css: null + }] + else + throw { fluenterror: HACKMYSTATUS.missingPackageJSON }; + @ + + + +###* +Determine if the theme supports the output format. +@method hasFormat +### +hasFormat: ( fmt ) -> _.has this.formats, fmt + + + +###* +Return the requested output format. +@method getFormat +### +getFormat = ( fmt ) -> @formats[ fmt ] + + +module.exports = JRSTheme; diff --git a/src/core/resume-factory.coffee b/src/core/resume-factory.coffee new file mode 100644 index 0000000..69223e3 --- /dev/null +++ b/src/core/resume-factory.coffee @@ -0,0 +1,115 @@ +###* +Definition of the ResumeFactory class. +@license MIT. See LICENSE.md for details. +@module core/resume-factory +### + + + +FS = require('fs') +HACKMYSTATUS = require('./status-codes') +HME = require('./event-codes') +ResumeConverter = require('fresh-jrs-converter') +chalk = require('chalk') +SyntaxErrorEx = require('../utils/syntax-error-ex') +_ = require('underscore') +require('string.prototype.startswith') + + + +###* +A simple factory class for FRESH and JSON Resumes. +@class ResumeFactory +### + +ResumeFactory = module.exports = + + + + ###* + Load one or more resumes from disk. + + @param {Object} opts An options object with settings for the factory as well + as passthrough settings for FRESHResume or JRSResume. Structure: + + { + format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null) + objectify: true, // FRESH/JRSResume or raw JSON? + inner: { // Passthru options for FRESH/JRSResume + sort: false + } + } + + ### + load: ( sources, opts, emitter ) -> + sources.map( (src) -> + @loadOne( src, opts, emitter ) + , @) + + + ###* Load a single resume from disk. ### + loadOne: ( src, opts, emitter ) -> + + toFormat = opts.format # Can be null + objectify = opts.objectify + + # Get the destination format. Can be 'fresh', 'jrs', or null/undefined. + toFormat && (toFormat = toFormat.toLowerCase().trim()) + + # Load and parse the resume JSON + info = _parse src, opts, emitter + return info if info.fluenterror + + # Determine the resume format: FRESH or JRS + json = info.json + isFRESH = json.meta && json.meta.format && json.meta.format.startsWith('FRESH@'); + orgFormat = if isFRESH then 'fresh' else 'jrs' + + # Convert between formats if necessary + if toFormat and ( orgFormat != toFormat ) + json = ResumeConverter[ 'to' + toFormat.toUpperCase() ]( json ) + + # Objectify the resume, that is, convert it from JSON to a FRESHResume + # or JRSResume object. + rez = null + if objectify + ResumeClass = require('../core/' + (toFormat || orgFormat) + '-resume'); + rez = new ResumeClass().parseJSON( json, opts.inner ); + rez.i().file = src; + + file: src + json: info.json + rez: rez + + +_parse = ( fileName, opts, eve ) -> + + rawData = null + try + + # Read the file + eve && eve.stat( HME.beforeRead, { file: fileName }); + rawData = FS.readFileSync( fileName, 'utf8' ); + eve && eve.stat( HME.afterRead, { file: fileName, data: rawData }); + + # Parse the file + eve && eve.stat HME.beforeParse, { data: rawData } + ret = { json: JSON.parse( rawData ) } + orgFormat = + if ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@') + then 'fresh' else 'jrs' + + eve && eve.stat HME.afterParse, { file: fileName, data: ret.json, fmt: orgFormat } + return ret + catch + # Can be ENOENT, EACCES, SyntaxError, etc. + ex = + fluenterror: if rawData then HACKMYSTATUS.parseError else HACKMYSTATUS.readError + inner: _error + raw: rawData + file: fileName + shouldExit: false + opts.quit && (ex.quit = true) + eve && eve.err ex.fluenterror, ex + throw ex if opts.throw + ex diff --git a/src/core/resume.json b/src/core/resume.json new file mode 100644 index 0000000..57bca12 --- /dev/null +++ b/src/core/resume.json @@ -0,0 +1,380 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Resume Schema", + "type": "object", + "additionalProperties": false, + "properties": { + "basics": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + }, + "label": { + "type": "string", + "description": "e.g. Web Developer" + }, + "picture": { + "type": "string", + "description": "URL (as per RFC 3986) to a picture in JPEG or PNG format" + }, + "email": { + "type": "string", + "description": "e.g. thomas@gmail.com", + "format": "email" + }, + "phone": { + "type": "string", + "description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923" + }, + "website": { + "type": "string", + "description": "URL (as per RFC 3986) to your website, e.g. personal homepage", + "format": "uri" + }, + "summary": { + "type": "string", + "description": "Write a short 2-3 sentence biography about yourself" + }, + "location": { + "type": "object", + "additionalProperties": true, + "properties": { + "address": { + "type": "string", + "description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li." + }, + "postalCode": { + "type": "string" + }, + "city": { + "type": "string" + }, + "countryCode": { + "type": "string", + "description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN" + }, + "region": { + "type": "string", + "description": "The general region where you live. Can be a US state, or a province, for instance." + } + } + }, + "profiles": { + "type": "array", + "description": "Specify any number of social networks that you participate in", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "network": { + "type": "string", + "description": "e.g. Facebook or Twitter" + }, + "username": { + "type": "string", + "description": "e.g. neutralthoughts" + }, + "url": { + "type": "string", + "description": "e.g. http://twitter.com/neutralthoughts" + } + } + } + } + } + }, + "work": { + "type": "array", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "company": { + "type": "string", + "description": "e.g. Facebook" + }, + "position": { + "type": "string", + "description": "e.g. Software Engineer" + }, + "website": { + "type": "string", + "description": "e.g. http://facebook.com", + "format": "uri" + }, + "startDate": { + "type": "string", + "description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29", + "format": "date" + }, + "endDate": { + "type": "string", + "description": "e.g. 2012-06-29", + "format": "date" + }, + "summary": { + "type": "string", + "description": "Give an overview of your responsibilities at the company" + }, + "highlights": { + "type": "array", + "description": "Specify multiple accomplishments", + "additionalItems": false, + "items": { + "type": "string", + "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising" + } + } + } + + } + }, + "volunteer": { + "type": "array", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "organization": { + "type": "string", + "description": "e.g. Facebook" + }, + "position": { + "type": "string", + "description": "e.g. Software Engineer" + }, + "website": { + "type": "string", + "description": "e.g. http://facebook.com", + "format": "uri" + }, + "startDate": { + "type": "string", + "description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29", + "format": "date" + }, + "endDate": { + "type": "string", + "description": "e.g. 2012-06-29", + "format": "date" + }, + "summary": { + "type": "string", + "description": "Give an overview of your responsibilities at the company" + }, + "highlights": { + "type": "array", + "description": "Specify multiple accomplishments", + "additionalItems": false, + "items": { + "type": "string", + "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising" + } + } + } + + } + }, + "education": { + "type": "array", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "institution": { + "type": "string", + "description": "e.g. Massachusetts Institute of Technology" + }, + "area": { + "type": "string", + "description": "e.g. Arts" + }, + "studyType": { + "type": "string", + "description": "e.g. Bachelor" + }, + "startDate": { + "type": "string", + "description": "e.g. 2014-06-29", + "format": "date" + }, + "endDate": { + "type": "string", + "description": "e.g. 2012-06-29", + "format": "date" + }, + "gpa": { + "type": "string", + "description": "grade point average, e.g. 3.67/4.0" + }, + "courses": { + "type": "array", + "description": "List notable courses/subjects", + "additionalItems": false, + "items": { + "type": "string", + "description": "e.g. H1302 - Introduction to American history" + } + } + } + + + } + }, + "awards": { + "type": "array", + "description": "Specify any awards you have received throughout your professional career", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "title": { + "type": "string", + "description": "e.g. One of the 100 greatest minds of the century" + }, + "date": { + "type": "string", + "description": "e.g. 1989-06-12", + "format": "date" + }, + "awarder": { + "type": "string", + "description": "e.g. Time Magazine" + }, + "summary": { + "type": "string", + "description": "e.g. Received for my work with Quantum Physics" + } + } + } + }, + "publications": { + "type": "array", + "description": "Specify your publications through your career", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. The World Wide Web" + }, + "publisher": { + "type": "string", + "description": "e.g. IEEE, Computer Magazine" + }, + "releaseDate": { + "type": "string", + "description": "e.g. 1990-08-01" + }, + "website": { + "type": "string", + "description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html" + }, + "summary": { + "type": "string", + "description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML." + } + } + } + }, + "skills": { + "type": "array", + "description": "List out your professional skill-set", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. Web Development" + }, + "level": { + "type": "string", + "description": "e.g. Master" + }, + "keywords": { + "type": "array", + "description": "List some keywords pertaining to this skill", + "additionalItems": false, + "items": { + "type": "string", + "description": "e.g. HTML" + } + } + } + } + }, + "languages": { + "type": "array", + "description": "List any other languages you speak", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "language": { + "type": "string", + "description": "e.g. English, Spanish" + }, + "fluency": { + "type": "string", + "description": "e.g. Fluent, Beginner" + } + } + } + }, + "interests": { + "type": "array", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. Philosophy" + }, + "keywords": { + "type": "array", + "additionalItems": false, + "items": { + "type": "string", + "description": "e.g. Friedrich Nietzsche" + } + } + } + + } + }, + "references": { + "type": "array", + "description": "List references you have received", + "additionalItems": false, + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. Timothy Cook" + }, + "reference": { + "type": "string", + "description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing." + } + } + + } + } + } +} diff --git a/src/core/status-codes.coffee b/src/core/status-codes.coffee new file mode 100644 index 0000000..6f32d9d --- /dev/null +++ b/src/core/status-codes.coffee @@ -0,0 +1,33 @@ +###* +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 diff --git a/src/generators/base-generator.coffee b/src/generators/base-generator.coffee new file mode 100644 index 0000000..5d15627 --- /dev/null +++ b/src/generators/base-generator.coffee @@ -0,0 +1,28 @@ +###* +Definition of the BaseGenerator class. +@module base-generator.js +@license MIT. See LICENSE.md for details. +### + + + +# Use J. Resig's nifty class implementation +Class = require '../utils/class' + + + +###* +The BaseGenerator class is the root of the generator hierarchy. Functionality +common to ALL generators lives here. +### + +BaseGenerator = module.exports = Class.extend + + ###* Base-class initialize. ### + init: ( outputFormat ) -> @format = outputFormat + + ###* Status codes. ### + codes: require '../core/status-codes' + + ###* Generator options. ### + opts: { } diff --git a/src/generators/html-generator.coffee b/src/generators/html-generator.coffee new file mode 100644 index 0000000..09888c1 --- /dev/null +++ b/src/generators/html-generator.coffee @@ -0,0 +1,30 @@ +###* +Definition of the HTMLGenerator class. +@license MIT. See LICENSE.md for details. +@module html-generator.js +### + + + +TemplateGenerator = require './template-generator' +FS = require 'fs-extra' +HTML = require 'html' +PATH = require 'path' +require 'string.prototype.endswith' + + + +HtmlGenerator = module.exports = TemplateGenerator.extend + + init: -> @_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 diff --git a/src/generators/html-pdf-cli-generator.coffee b/src/generators/html-pdf-cli-generator.coffee new file mode 100644 index 0000000..6827e77 --- /dev/null +++ b/src/generators/html-pdf-cli-generator.coffee @@ -0,0 +1,91 @@ +###* +Definition of the HtmlPdfCLIGenerator class. +@module html-pdf-generator.js +@license MIT. See LICENSE.md for details. +### + + + +TemplateGenerator = require('./template-generator') +FS = require('fs-extra') +HTML = require( 'html' ) +PATH = require('path') +SPAWN = require('../utils/safe-spawn') +SLASH = require('slash'); + + + +###* +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. +### + +HtmlPdfCLIGenerator = module.exports = TemplateGenerator.extend + + + init: () -> @_super 'pdf', 'html' + + + ###* Generate the binary PDF. ### + onBeforeSave: ( info ) -> + try + safe_eng = info.opts.pdf || 'wkhtmltopdf'; + if safe_eng != 'none' + engines[ safe_eng ].call this, info.mk, info.outputFile + return null # halt further processing + catch ex + # { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', ... } + # { [Error: ENOENT] } + if ex.inner && ex.inner.code == 'ENOENT' + throw + fluenterror: this.codes.notOnPath + inner: ex.inner + engine: ex.cmd, + stack: ex.inner && ex.inner.stack + else + throw + fluenterror: this.codes.pdfGeneration + inner: ex + stack: ex.stack + + + +# TODO: Move each engine to a separate module +engines = + + + + ###* + Generate a PDF from HTML using wkhtmltopdf's CLI interface. + Spawns a child process with `wkhtmltopdf `. 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) -> + # Save the markup to a temporary file + tempFile = fOut.replace /\.pdf$/i, '.pdf.html' + FS.writeFileSync tempFile, markup, 'utf8' + info = SPAWN 'wkhtmltopdf', [ tempFile, fOut ] + + + + ###* + Generate a PDF from HTML using Phantom's CLI interface. + Spawns a child process with `phantomjs