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

Finish HackMyCore reshaping.

Reintroduce HackMyCore, dropping the interim submodule, and reorganize
and improve tests.
This commit is contained in:
hacksalot
2016-01-29 15:23:57 -05:00
parent e9971eb882
commit 0f65e4c9f3
130 changed files with 5384 additions and 337 deletions

75
src/verbs/analyze.coffee Normal file
View File

@ -0,0 +1,75 @@
###*
Implementation of the 'analyze' verb for HackMyResume.
@module verbs/analyze
@license MIT. See LICENSE.md for details.
###
MKDIRP = require('mkdirp')
PATH = require('path')
HMEVENT = require('../core/event-codes')
HMSTATUS = require('../core/status-codes')
_ = require('underscore')
ResumeFactory = require('../core/resume-factory')
Verb = require('../verbs/verb')
chalk = require('chalk')
AnalyzeVerb = module.exports = Verb.extend
init: -> @._super 'analyze'
invoke: ->
@.stat HMEVENT.begin, { cmd: 'analyze' }
analyze.apply @, arguments
@.stat HMEVENT.end
###*
Run the 'analyze' command.
###
analyze = ( sources, dst, opts ) ->
if !sources || !sources.length
throw
fluenterror: HMSTATUS.resumeNotFound
quit: true
nlzrs = _loadInspectors()
_.each(sources, (src) ->
result = ResumeFactory.loadOne src, format: 'FRESH', objectify: true, @
if result.fluenterror
this.setError result.fluenterror, result
else
_analyze.call @, result, nlzrs, opts
, @)
###*
Analyze a single resume.
###
_analyze = ( resumeObject, nlzrs, opts ) ->
rez = resumeObject.rez
safeFormat =
if rez.meta and rez.meta.format and rez.meta.format.startsWith 'FRESH'
then 'FRESH' else 'JRS'
this.stat( HMEVENT.beforeAnalyze, { fmt: safeFormat, file: resumeObject.file })
info = _.mapObject nlzrs, (val, key) -> val.run rez
this.stat HMEVENT.afterAnalyze, { info: info }
###*
Load inspectors.
###
_loadInspectors = ->
totals: require '../inspectors/totals-inspector'
coverage: require '../inspectors/gap-inspector'
keywords: require '../inspectors/keyword-inspector'

340
src/verbs/build.coffee Normal file
View File

@ -0,0 +1,340 @@
###*
Implementation of the 'build' verb for HackMyResume.
@module verbs/build
@license MIT. See LICENSE.md for details.
###
_ = require('underscore')
PATH = require('path')
FS = require('fs')
MD = require('marked')
MKDIRP = require('mkdirp')
extend = require('extend')
parsePath = require('parse-filepath')
RConverter = require('fresh-jrs-converter')
HMSTATUS = require('../core/status-codes')
HMEVENT = require('../core/event-codes')
RTYPES =
FRESH: require('../core/fresh-resume')
JRS: require('../core/jrs-resume')
_opts = require('../core/default-options')
FRESHTheme = require('../core/fresh-theme')
JRSTheme = require('../core/jrs-theme')
ResumeFactory = require('../core/resume-factory')
_fmts = require('../core/default-formats')
Verb = require('../verbs/verb')
_err = null
_log = null
_rezObj = null
build = null
prep = null
single = null
verifyOutputs = null
addFreebieFormats = null
expand = null
verifyTheme = null
loadTheme = null
###* An invokable resume generation command. ###
BuildVerb = module.exports = Verb.extend
###* Create a new build verb. ###
init: () -> @._super 'build'
###* Invoke the Build command. ###
invoke: ->
@stat HMEVENT.begin, { cmd: 'build' }
ret = build.apply @, arguments
@stat HMEVENT.end
return ret
###*
Given a source resume in FRESH or JRS format, a destination resume path, and a
theme file, generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
###
build = ( src, dst, opts ) ->
if !src || !src.length
@err HMSTATUS.resumeNotFound, { quit: true }
prep src, dst, opts
# Load input resumes as JSON...
sheetObjects = ResumeFactory.load(src, {
format: null, objectify: false, quit: true, inner: { sort: _opts.sort }
}, @);
# Explicit check for any resume loading errors...
if !sheetObjects || _.some( sheetObjects, (so) -> return so.fluenterror )
return null
sheets = sheetObjects.map((r) -> return r.json )
# Load the theme...
theme = null
@stat HMEVENT.beforeTheme, { theme: _opts.theme }
try
tFolder = verifyTheme.call @, _opts.theme
theme = _opts.themeObj = loadTheme tFolder
catch ex
newEx =
fluenterror: HMSTATUS.themeLoad
inner: ex
attempted: _opts.theme
this.err HMSTATUS.themeLoad, newEx
return null
@stat HMEVENT.afterTheme, { theme: theme }
# Check for invalid outputs...
inv = verifyOutputs.call @, dst, theme
if inv && inv.length
@err HMSTATUS.invalidFormat, { data: inv, theme: theme }
## Merge input resumes, yielding a single source resume.
rez = null
if sheets.length > 1
isFRESH = !sheets[0].basics
mixed = _.any sheets, (s) -> return if isFRESH then s.basics else !s.basics
@stat HMEVENT.beforeMerge, { f: _.clone(sheetObjects), mixed: mixed }
if mixed
this.err HMSTATUS.mixedMerge
rez = _.reduceRight sheets, ( a, b, idx ) ->
extend( true, b, a )
@stat HMEVENT.afterMerge, { r: rez }
else
rez = sheets[0];
# Convert the merged source resume to the theme's format, if necessary
orgFormat = if rez.basics then 'JRS' else 'FRESH';
toFormat = if theme.render then 'JRS' else 'FRESH';
if toFormat != orgFormat
@stat HMEVENT.beforeInlineConvert
rez = RConverter[ 'to' + toFormat ]( rez );
@stat HMEVENT.afterInlineConvert, { file: sheetObjects[0].file, fmt: toFormat }
# Add freebie formats to the theme
addFreebieFormats theme
@stat HMEVENT.applyTheme, { r: rez, theme: theme }
# Load the resume into a FRESHResume or JRSResume object
_rezObj = new (RTYPES[ toFormat ])().parseJSON( rez );
# Expand output resumes...
targets = expand( dst, theme );
# Run the transformation!
_.each targets, (t) ->
t.final = single.call this, t, theme, targets
, @
# Don't send the client back empty-handed
sheet: _rezObj
targets: targets
processed: targets
###*
Prepare for a BUILD run.
###
prep = ( src, dst, opts ) ->
# Cherry-pick options //_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
_opts.prettify = opts.prettify is true
_opts.css = opts.css
_opts.pdf = opts.pdf
_opts.wrap = opts.wrap || 60
_opts.stitles = opts.sectionTitles
_opts.tips = opts.tips
_opts.errHandler = opts.errHandler
_opts.noTips = opts.noTips
_opts.debug = opts.debug
_opts.sort = opts.sort
# If two or more files are passed to the GENERATE command and the TO
# keyword is omitted, the last file specifies the output file.
( src.length > 1 && ( !dst || !dst.length ) ) && dst.push( src.pop() )
return
###*
Generate a single target resume such as "out/rez.html" or "out/rez.doc".
TODO: Refactor.
@param targInfo Information for the target resume.
@param theme A FRESHTheme or JRSTheme object.
###
single = ( targInfo, theme, finished ) ->
ret = null
ex = null
f = targInfo.file
try
if !targInfo.fmt
return
fType = targInfo.fmt.outFormat
fName = PATH.basename f, '.' + fType
theFormat = null
@.stat HMEVENT.beforeGenerate,
fmt: targInfo.fmt.outFormat
file: PATH.relative(process.cwd(), f)
# If targInfo.fmt.files exists, this format is backed by a document.
# Fluent/FRESH themes are handled here.
if targInfo.fmt.files && targInfo.fmt.files.length
theFormat = _fmts.filter(
(fmt) -> return fmt.name == targInfo.fmt.outFormat )[0];
MKDIRP.sync( PATH.dirname( f ) ); # Ensure dest folder exists;
_opts.targets = finished;
ret = theFormat.gen.generate( _rezObj, f, _opts );
# Otherwise this is an ad-hoc format (JSON, YML, or PNG) that every theme
# gets "for free".
else
theFormat = _fmts.filter( (fmt) ->
return fmt.name == targInfo.fmt.outFormat
)[0];
outFolder = PATH.dirname f
MKDIRP.sync( outFolder ); # Ensure dest folder exists;
ret = theFormat.gen.generate( _rezObj, f, _opts );
catch e
# Catch any errors caused by generating this file and don't let them
# propagate -- typically we want to continue processing other formats
# even if this format failed.
ex = e;
this.stat HMEVENT.afterGenerate,
fmt: targInfo.fmt.outFormat,
file: PATH.relative( process.cwd(), f ),
error: ex
if ex
if ex.fluenterror
this.err( ex.fluenterror, ex );
else
this.err( HMSTATUS.generateError, { inner: ex } );
return ret
###*
Ensure that user-specified outputs/targets are valid.
###
verifyOutputs = ( targets, theme ) ->
@.stat HMEVENT.verifyOutputs, { targets: targets, theme: theme }
_.reject targets.map( ( t ) ->
pathInfo = parsePath t
{
format: pathInfo.extname.substr(1)
}),
(t) -> t.format == 'all' || theme.hasFormat( t.format )
###*
Reinforce the chosen theme with "freebie" formats provided by HackMyResume.
A "freebie" format is an output format such as JSON, YML, or PNG that can be
generated directly from the resume model or from one of the theme's declared
output formats. For example, the PNG format can be generated for any theme
that declares an HTML format; the theme doesn't have to provide an explicit
PNG template.
@param theTheme A FRESHTheme or JRSTheme object.
###
addFreebieFormats = ( theTheme ) ->
# Add freebie formats (JSON, YAML, PNG) every theme gets...
# Add HTML-driven PNG only if the theme has an HTML format.
theTheme.formats.json = theTheme.formats.json || {
freebie: true, title: 'json', outFormat: 'json', pre: 'json',
ext: 'json', path: null, data: null
}
theTheme.formats.yml = theTheme.formats.yml || {
freebie: true, title: 'yaml', outFormat: 'yml', pre: 'yml',
ext: 'yml', path: null, data: null
}
if theTheme.formats.html && !theTheme.formats.png
theTheme.formats.png = {
freebie: true, title: 'png', outFormat: 'png',
ext: 'yml', path: null, data: null
}
return
###*
Expand output files. For example, "foo.all" should be expanded to
["foo.html", "foo.doc", "foo.pdf", "etc"].
@param dst An array of output files as specified by the user.
@param theTheme A FRESHTheme or JRSTheme object.
###
expand = ( dst, theTheme ) ->
# Set up the destination collection. It's either the array of files passed
# by the user or 'out/resume.all' if no targets were specified.
destColl = (dst && dst.length && dst) || [PATH.normalize('out/resume.all')];
# Assemble an array of expanded target files... (can't use map() here)
targets = [];
destColl.forEach (t) ->
to = PATH.resolve(t)
pa = parsePath(to)
fmat = pa.extname || '.all';
targets.push.apply( targets,
if fmat == '.all'
then Object.keys( theTheme.formats ).map( ( k ) ->
z = theTheme.formats[k]
return { file: to.replace( /all$/g, z.outFormat ), fmt: z }
)
else [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]
)
targets
###*
Verify the specified theme name/path.
###
verifyTheme = ( themeNameOrPath ) ->
tFolder = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/themes/',
themeNameOrPath
)
exists = require('path-exists').sync
if !exists( tFolder )
tFolder = PATH.resolve themeNameOrPath
if !exists tFolder
this.err HMSTATUS.themeNotFound, { data: _opts.theme }
tFolder
###*
Load the specified theme, which could be either a FRESH theme or a JSON Resume
theme.
###
loadTheme = ( tFolder ) ->
# Create a FRESH or JRS theme object
theTheme =
if _opts.theme.indexOf('jsonresume-theme-') > -1
then new JRSTheme().open(tFolder) else new FRESHTheme().open( tFolder );
# Cache the theme object
_opts.themeObj = theTheme;
theTheme

77
src/verbs/convert.coffee Normal file
View File

@ -0,0 +1,77 @@
###*
Implementation of the 'convert' verb for HackMyResume.
@module verbs/convert
@license MIT. See LICENSE.md for details.
###
ResumeFactory = require('../core/resume-factory')
chalk = require('chalk')
Verb = require('../verbs/verb')
HMSTATUS = require('../core/status-codes')
_ = require('underscore')
HMEVENT = require('../core/event-codes');
ConvertVerb = module.exports = Verb.extend
init: -> @._super 'convert'
invoke: ->
@.stat HMEVENT.begin, { cmd: 'convert' }
convert.apply @, arguments
@.stat HMEVENT.end
###*
Convert between FRESH and JRS formats.
###
convert = ( srcs, dst, opts ) ->
# Housekeeping
throw { fluenterror: 6, quit: true } if !srcs || !srcs.length
if !dst || !dst.length
if srcs.length == 1
throw { fluenterror: HMSTATUS.inputOutputParity, quit: true };
else if srcs.length == 2
dst = dst || []; dst.push( srcs.pop() )
else
throw fluenterror: HMSTATUS.inputOutputParity, quit: true
if srcs && dst && srcs.length && dst.length && srcs.length != dst.length
throw fluenterror: HMSTATUS.inputOutputParity quit: true
# Load source resumes
_.each(srcs, ( src, idx ) ->
# Load the resume
rinfo = ResumeFactory.loadOne src,
format: null, objectify: true, throw: false
# If a load error occurs, report it and move on to the next file (if any)
if rinfo.fluenterror
this.err rinfo.fluenterror, rinfo
return
s = rinfo.rez
srcFmt =
if ((s.basics && s.basics.imp) || s.imp).orgFormat == 'JRS'
then 'JRS' else 'FRESH'
targetFormat = if srcFmt == 'JRS' then 'FRESH' else 'JRS'
this.stat HMEVENT.beforeConvert,
srcFile: rinfo.file
srcFmt: srcFmt
dstFile: dst[idx]
dstFmt: targetFormat
# Save it to the destination format
s.saveAs dst[idx], targetFormat
return
, @)
return

46
src/verbs/create.coffee Normal file
View File

@ -0,0 +1,46 @@
###*
Implementation of the 'create' verb for HackMyResume.
@module verbs/create
@license MIT. See LICENSE.md for details.
###
MKDIRP = require('mkdirp')
PATH = require('path')
chalk = require('chalk')
Verb = require('../verbs/verb')
_ = require('underscore')
HMSTATUS = require('../core/status-codes')
HMEVENT = require('../core/event-codes')
CreateVerb = module.exports = Verb.extend
init: -> @._super('new')
invoke: ->
@.stat HMEVENT.begin, { cmd: 'create' }
create.apply @, arguments
@.stat HMEVENT.begin, { cmd: 'convert' }
return
###*
Create a new empty resume in either FRESH or JRS format.
###
create = ( src, dst, opts ) ->
if !src || !src.length
throw { fluenterror: HMSTATUS.createNameMissing, quit: true }
_.each( src, ( t ) ->
safeFmt = opts.format.toUpperCase()
@.stat HMEVENT.beforeCreate, { fmt: safeFmt, file: t }
MKDIRP.sync PATH.dirname( t ) # Ensure dest folder exists;
RezClass = require '../core/' + safeFmt.toLowerCase() + '-resume'
RezClass.default().save t
@.stat( HMEVENT.afterCreate, { fmt: safeFmt, file: t } )
, @)
return

67
src/verbs/peek.coffee Normal file
View File

@ -0,0 +1,67 @@
###*
Implementation of the 'peek' verb for HackMyResume.
@module verbs/peek
@license MIT. See LICENSE.md for details.
###
Verb = require('../verbs/verb')
_ = require('underscore')
__ = require('lodash')
safeLoadJSON = require('../utils/safe-json-loader')
HMSTATUS = require('../core/status-codes')
HMEVENT = require('../core/event-codes')
PeekVerb = module.exports = Verb.extend
init: -> @._super('peek')
invoke: ->
@.stat HMEVENT.begin, { cmd: 'peek' }
peek.apply @, arguments
@.stat HMEVENT.end
###* Peek at a resume, resume section, or resume field. ###
peek = ( src, dst, opts ) ->
if !src || !src.length
throw: fluenterror: HMSTATUS.resumeNotFound
objPath = (dst && dst[0]) || ''
_.each src, ( t ) ->
# Fire the 'beforePeek' event 2nd, so we have error/warning/success
@.stat HMEVENT.beforePeek, { file: t, target: objPath }
# Load the input file JSON 1st
obj = safeLoadJSON t
# Fetch the requested object path (or the entire file)
tgt = null;
if !obj.ex
tgt = if objPath then __.get obj.json, objPath else obj.json;
# Fire the 'afterPeek' event with collected info
@.stat HMEVENT.afterPeek,
file: t
requested: objPath
target: tgt
error: obj.ex
# safeLoadJSON can only return a READ error or a PARSE error
if obj.ex
errCode = if obj.ex.operation == 'parse' then HMSTATUS.parseError else HMSTATUS.readError
if errCode == HMSTATUS.readError
obj.ex.quiet = true
@setError errCode, obj.ex
@err errCode, obj.ex
, @
return

92
src/verbs/validate.coffee Normal file
View File

@ -0,0 +1,92 @@
###*
Implementation of the 'validate' verb for HackMyResume.
@module verbs/validate
@license MIT. See LICENSE.md for details.
###
FS = require 'fs'
ResumeFactory = require '../core/resume-factory'
SyntaxErrorEx = require '../utils/syntax-error-ex'
chalk = require 'chalk'
Verb = require '../verbs/verb'
HMSTATUS = require '../core/status-codes'
HMEVENT = require '../core/event-codes'
_ = require 'underscore'
safeLoadJSON = require '../utils/safe-json-loader'
###* An invokable resume validation command. ###
ValidateVerb = module.exports = Verb.extend
init: -> @_super 'validate'
invoke: ->
@stat HMEVENT.begin, { cmd: 'validate' }
ret = validate.apply @, arguments
@stat HMEVENT.end
return ret
###* Validate 1 to N resumes in FRESH or JSON Resume format. ###
validate = (sources, unused, opts) ->
if !sources || !sources.length
throw { fluenterror: HMSTATUS.resumeNotFoundAlt, quit: true }
validator = require 'is-my-json-valid'
schemas =
fresh: require 'fresca'
jars: require '../core/resume.json'
# Validate input resumes. Return a { file: <f>, isValid: <v>} object for
# each resume valid, invalid, or broken.
_.map sources, (t) ->
ret = file: t, isValid: false
# Load the input file JSON 1st
obj = safeLoadJSON t
if !obj.ex
# Successfully read the resume. Now parse it as JSON.
json = obj.json
fmt = if json.basics then 'jrs' else 'fresh'
errors = []
try
validate = validator schemas[ fmt ], { # Note [1]
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
catch
ret.ex = _error
# safeLoadJSON can only return a READ error or a PARSE error
else
errCode = if obj.ex.operation == 'parse' then HMSTATUS.parseError else HMSTATUS.readError
if errCode == HMSTATUS.readError
obj.ex.quiet = true
@setError errCode, obj.ex
@err errCode, obj.ex
@stat HMEVENT.afterValidate,
file: t
isValid: ret.isValid
fmt: fmt?.replace 'jars', 'JSON Resume'
errors: errors
if opts.assert and !ret.isValid
throw fluenterror: HMSTATUS.invalid, shouldExit: true
console.log '1111'
return ret
, @

71
src/verbs/verb.coffee Normal file
View File

@ -0,0 +1,71 @@
###*
Definition of the Verb class.
@module verbs/verb
@license MIT. See LICENSE.md for details.
###
# Use J. Resig's nifty class implementation
Class = require '../utils/class'
EVENTS = require 'events'
###*
An instantiation of a HackMyResume command.
@class Verb
###
Verb = module.exports = Class.extend
###* Constructor. Automatically called at creation. ###
init: ( moniker ) ->
@.moniker = moniker
@.emitter = new EVENTS.EventEmitter()
return
###* Forward subscriptions to the event emitter. ###
on: ->
this.emitter.on.apply @.emitter, arguments
###* Fire an arbitrary event, scoped to "hmr:". ###
fire: (evtName, payload) ->
payload = payload || { }
payload.cmd = this.moniker
this.emitter.emit 'hmr:' + evtName, payload
true
###* Handle an error condition. ###
err: ( errorCode, payload, hot ) ->
payload = payload || { }
payload.sub = payload.fluenterror = errorCode
payload.throw = hot
this.fire 'error', payload
if hot
throw payload
true
###* Fire the 'hmr:status' error event. ###
stat: ( subEvent, payload ) ->
payload = payload || { }
payload.sub = subEvent
this.fire 'status', payload
true
###* Associate error info with the invocation. ###
setError: ( code, obj ) ->
@errorCode = code
@errorObj = obj
return