1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2025-07-13 21:51:13 +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

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