402 lines
12 KiB
CoffeeScript
402 lines
12 KiB
CoffeeScript
###*
|
|
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. ###
|
|
module.exports = class BuildVerb extends Verb
|
|
|
|
###* Create a new build verb. ###
|
|
constructor: () -> super 'build', _build
|
|
|
|
|
|
|
|
###*
|
|
Given a source resume in FRESH or JRS format, a destination resume path, and a
|
|
theme file, generate 0..N resumes in the desired formats.
|
|
@param src Path to the source JSON resume file: "rez/resume.json".
|
|
@param dst An array of paths to the target resume file(s).
|
|
@param opts Generation options.
|
|
###
|
|
_build = ( src, dst, opts ) ->
|
|
|
|
if !src || !src.length
|
|
@err HMSTATUS.resumeNotFound, quit: true
|
|
return null
|
|
|
|
_prep.call @, src, dst, opts
|
|
|
|
# Load input resumes as JSON...
|
|
sheetObjects = ResumeFactory.load src,
|
|
format: null, objectify: false, quit: true, inner: {
|
|
sort: _opts.sort
|
|
private: _opts.private
|
|
}
|
|
, @
|
|
|
|
# Explicit check for any resume loading errors...
|
|
problemSheets = _.filter sheetObjects, (so) -> so.fluenterror
|
|
if problemSheets and problemSheets.length
|
|
problemSheets[0].quit = true # can't go on
|
|
@err problemSheets[0].fluenterror, problemSheets[0]
|
|
return null
|
|
|
|
# Get the collection of raw JSON sheets
|
|
sheets = sheetObjects.map (r) -> r.json
|
|
|
|
# Load the theme...
|
|
theme = null
|
|
@stat HMEVENT.beforeTheme, { theme: _opts.theme }
|
|
try
|
|
tFolder = _verifyTheme.call @, _opts.theme
|
|
if tFolder.fluenterror
|
|
tFolder.quit = true
|
|
@err tFolder.fluenterror, tFolder
|
|
return
|
|
theme = _opts.themeObj = _loadTheme tFolder
|
|
_addFreebieFormats theme
|
|
catch err
|
|
newEx =
|
|
fluenterror: HMSTATUS.themeLoad
|
|
inner: err
|
|
attempted: _opts.theme
|
|
quit: true
|
|
@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, quit: true
|
|
return null
|
|
|
|
## Merge input resumes, yielding a single source resume...
|
|
rez = null
|
|
if sheets.length > 1
|
|
isFRESH = !sheets[0].basics
|
|
mixed = _.any sheets, (s) -> return if isFRESH then s.basics else !s.basics
|
|
@stat HMEVENT.beforeMerge, { f: _.clone(sheetObjects), mixed: mixed }
|
|
if mixed
|
|
@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 }
|
|
|
|
# Announce the theme
|
|
@stat HMEVENT.applyTheme, r: rez, theme: theme
|
|
|
|
# Load the resume into a FRESHResume or JRSResume object
|
|
_rezObj = new (RTYPES[ toFormat ])().parseJSON( rez, private: _opts.private );
|
|
|
|
# Expand output resumes...
|
|
targets = _expand dst, theme
|
|
|
|
# Run the transformation!
|
|
_.each targets, (t) ->
|
|
return { } if @hasError() and opts.assert
|
|
t.final = _single.call @, t, theme, targets
|
|
if t.final?.fluenterror
|
|
t.final.quit = opts.assert
|
|
@err t.final.fluenterror, t.final
|
|
return
|
|
, @
|
|
|
|
results =
|
|
sheet: _rezObj
|
|
targets: targets
|
|
processed: targets
|
|
|
|
if @hasError() and !opts.assert
|
|
@reject results
|
|
else if !@hasError()
|
|
@resolve results
|
|
|
|
results
|
|
|
|
|
|
|
|
###*
|
|
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.private = opts.private is true
|
|
_opts.noescape = opts.noescape 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
|
|
_opts.wkhtmltopdf = opts.wkhtmltopdf
|
|
that = @
|
|
|
|
# Set up callbacks for internal generators
|
|
_opts.onTransform = (info) ->
|
|
that.stat HMEVENT.afterTransform, info; return
|
|
_opts.beforeWrite = (info) ->
|
|
that.stat HMEVENT.beforeWrite, info; return
|
|
_opts.afterWrite = (info) ->
|
|
that.stat HMEVENT.afterWrite, info; return
|
|
|
|
|
|
# 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
|
|
|
|
_opts.targets = finished
|
|
|
|
# If targInfo.fmt.files exists, this format is backed by a document.
|
|
# Fluent/FRESH themes are handled here.
|
|
if targInfo.fmt.files && targInfo.fmt.files.length
|
|
theFormat = _fmts.filter( (fmt) ->
|
|
return fmt.name == targInfo.fmt.outFormat
|
|
)[0];
|
|
MKDIRP.sync PATH.dirname( f )
|
|
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
|
|
ex = e
|
|
|
|
this.stat HMEVENT.afterGenerate,
|
|
fmt: targInfo.fmt.outFormat
|
|
file: PATH.relative process.cwd(), f
|
|
error: ex
|
|
|
|
if ex
|
|
if ex.fluenterror
|
|
ret = ex
|
|
else
|
|
ret = fluenterror: HMSTATUS.generateError, inner: ex
|
|
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 ) ->
|
|
|
|
# First, see if this is one of the predefined FRESH themes. There are only a
|
|
# handful of these, but they may change over time, so we need to query
|
|
# the official source of truth: the fresh-themes repository, which mounts the
|
|
# themes conveniently by name to the module object, and which is embedded
|
|
# locally inside the HackMyResume installation.
|
|
themesObj = require 'fresh-themes'
|
|
if _.has themesObj.themes, themeNameOrPath
|
|
tFolder = PATH.join(
|
|
parsePath( require.resolve('fresh-themes') ).dirname,
|
|
'/themes/',
|
|
themeNameOrPath
|
|
)
|
|
else
|
|
# Otherwsie it's a path to an arbitrary FRESH or JRS theme sitting somewhere
|
|
# on the user's system (or, in the future, at a URI).
|
|
tFolder = PATH.resolve themeNameOrPath
|
|
|
|
# In either case, make sure the theme folder exists
|
|
exists = require('path-exists').sync
|
|
if exists tFolder
|
|
tFolder
|
|
else
|
|
fluenterror: HMSTATUS.themeNotFound, data: _opts.theme
|
|
|
|
|
|
|
|
###*
|
|
Load the specified theme, which could be either a FRESH theme or a JSON Resume
|
|
theme (or both).
|
|
###
|
|
_loadTheme = ( tFolder ) ->
|
|
|
|
themeJsonPath = PATH.join tFolder, 'theme.json' # [^1]
|
|
exists = require('path-exists').sync
|
|
|
|
# Create a FRESH or JRS theme object
|
|
theTheme =
|
|
if exists themeJsonPath
|
|
then new FRESHTheme().open tFolder
|
|
else new JRSTheme().open tFolder
|
|
|
|
# Cache the theme object
|
|
_opts.themeObj = theTheme;
|
|
theTheme
|
|
|
|
|
|
# FOOTNOTES
|
|
# ------------------------------------------------------------------------------
|
|
# [^1] We don't know ahead of time whether this is a FRESH or JRS theme.
|
|
# However, all FRESH themes have a theme.json file, so we'll use that as a
|
|
# canary for now, as an interim solution.
|
|
#
|
|
# Unfortunately, with the exception of FRESH's theme.json, both FRESH and
|
|
# JRS themes are free-form and don't have a ton of reliable distinguishing
|
|
# marks, which makes a simple task like ad hoc theme detection harder than
|
|
# it should be to do cleanly.
|
|
#
|
|
# Another complicating factor is that it's possible for a theme to be BOTH.
|
|
# That is, a single set of theme files can serve as a FRESH theme -and- a
|
|
# JRS theme.
|
|
#
|
|
# TODO: The most robust way to deal with all these issues is with a strong
|
|
# theme validator. If a theme structure validates as a particular theme
|
|
# type, then for all intents and purposes, it IS a theme of that type.
|