mirror of
https://github.com/JuanCanham/HackMyResume.git
synced 2024-11-23 00:40:10 +00:00
417 lines
10 KiB
JavaScript
417 lines
10 KiB
JavaScript
|
|
||
|
/**
|
||
|
Implementation of the 'build' verb for HackMyResume.
|
||
|
@module verbs/build
|
||
|
@license MIT. See LICENSE.md for details.
|
||
|
*/
|
||
|
|
||
|
(function() {
|
||
|
var BuildVerb, FRESHTheme, FS, HMEVENT, HMSTATUS, JRSTheme, MD, MKDIRP, PATH, RConverter, RTYPES, ResumeFactory, Verb, _, _err, _fmts, _log, _opts, _rezObj, addFreebieFormats, build, expand, extend, loadTheme, parsePath, prep, single, verifyOutputs, verifyTheme;
|
||
|
|
||
|
_ = 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: function() {
|
||
|
return this._super('build');
|
||
|
},
|
||
|
|
||
|
/** Invoke the Build command. */
|
||
|
invoke: function() {
|
||
|
var ret;
|
||
|
this.stat(HMEVENT.begin, {
|
||
|
cmd: 'build'
|
||
|
});
|
||
|
ret = build.apply(this, arguments);
|
||
|
this.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 = function(src, dst, opts) {
|
||
|
var ex, inv, isFRESH, mixed, newEx, orgFormat, rez, sheetObjects, sheets, tFolder, targets, theme, toFormat;
|
||
|
if (!src || !src.length) {
|
||
|
this.err(HMSTATUS.resumeNotFound, {
|
||
|
quit: true
|
||
|
});
|
||
|
}
|
||
|
prep(src, dst, opts);
|
||
|
sheetObjects = ResumeFactory.load(src, {
|
||
|
format: null,
|
||
|
objectify: false,
|
||
|
quit: true,
|
||
|
inner: {
|
||
|
sort: _opts.sort
|
||
|
}
|
||
|
}, this);
|
||
|
if (!sheetObjects || _.some(sheetObjects, function(so) {
|
||
|
return so.fluenterror;
|
||
|
})) {
|
||
|
return null;
|
||
|
}
|
||
|
sheets = sheetObjects.map(function(r) {
|
||
|
return r.json;
|
||
|
});
|
||
|
theme = null;
|
||
|
this.stat(HMEVENT.beforeTheme, {
|
||
|
theme: _opts.theme
|
||
|
});
|
||
|
try {
|
||
|
tFolder = verifyTheme.call(this, _opts.theme);
|
||
|
theme = _opts.themeObj = loadTheme(tFolder);
|
||
|
} catch (_error) {
|
||
|
ex = _error;
|
||
|
newEx = {
|
||
|
fluenterror: HMSTATUS.themeLoad,
|
||
|
inner: ex,
|
||
|
attempted: _opts.theme
|
||
|
};
|
||
|
this.err(HMSTATUS.themeLoad, newEx);
|
||
|
return null;
|
||
|
}
|
||
|
this.stat(HMEVENT.afterTheme, {
|
||
|
theme: theme
|
||
|
});
|
||
|
inv = verifyOutputs.call(this, dst, theme);
|
||
|
if (inv && inv.length) {
|
||
|
this.err(HMSTATUS.invalidFormat, {
|
||
|
data: inv,
|
||
|
theme: theme
|
||
|
});
|
||
|
}
|
||
|
rez = null;
|
||
|
if (sheets.length > 1) {
|
||
|
isFRESH = !sheets[0].basics;
|
||
|
mixed = _.any(sheets, function(s) {
|
||
|
if (isFRESH) {
|
||
|
return s.basics;
|
||
|
} else {
|
||
|
return !s.basics;
|
||
|
}
|
||
|
});
|
||
|
this.stat(HMEVENT.beforeMerge, {
|
||
|
f: _.clone(sheetObjects),
|
||
|
mixed: mixed
|
||
|
});
|
||
|
if (mixed) {
|
||
|
this.err(HMSTATUS.mixedMerge);
|
||
|
}
|
||
|
rez = _.reduceRight(sheets, function(a, b, idx) {
|
||
|
return extend(true, b, a);
|
||
|
});
|
||
|
this.stat(HMEVENT.afterMerge, {
|
||
|
r: rez
|
||
|
});
|
||
|
} else {
|
||
|
rez = sheets[0];
|
||
|
}
|
||
|
orgFormat = rez.basics ? 'JRS' : 'FRESH';
|
||
|
toFormat = theme.render ? 'JRS' : 'FRESH';
|
||
|
if (toFormat !== orgFormat) {
|
||
|
this.stat(HMEVENT.beforeInlineConvert);
|
||
|
rez = RConverter['to' + toFormat](rez);
|
||
|
this.stat(HMEVENT.afterInlineConvert, {
|
||
|
file: sheetObjects[0].file,
|
||
|
fmt: toFormat
|
||
|
});
|
||
|
}
|
||
|
addFreebieFormats(theme);
|
||
|
this.stat(HMEVENT.applyTheme, {
|
||
|
r: rez,
|
||
|
theme: theme
|
||
|
});
|
||
|
_rezObj = new RTYPES[toFormat]().parseJSON(rez);
|
||
|
targets = expand(dst, theme);
|
||
|
_.each(targets, function(t) {
|
||
|
return t.final = single.call(this, t, theme, targets);
|
||
|
}, this);
|
||
|
return {
|
||
|
sheet: _rezObj,
|
||
|
targets: targets,
|
||
|
processed: targets
|
||
|
};
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
Prepare for a BUILD run.
|
||
|
*/
|
||
|
|
||
|
prep = function(src, dst, opts) {
|
||
|
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
|
||
|
_opts.prettify = opts.prettify === 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;
|
||
|
(src.length > 1 && (!dst || !dst.length)) && dst.push(src.pop());
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
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 = function(targInfo, theme, finished) {
|
||
|
var e, ex, f, fName, fType, outFolder, ret, theFormat;
|
||
|
ret = null;
|
||
|
ex = null;
|
||
|
f = targInfo.file;
|
||
|
try {
|
||
|
if (!targInfo.fmt) {
|
||
|
return;
|
||
|
}
|
||
|
fType = targInfo.fmt.outFormat;
|
||
|
fName = PATH.basename(f, '.' + fType);
|
||
|
theFormat = null;
|
||
|
this.stat(HMEVENT.beforeGenerate, {
|
||
|
fmt: targInfo.fmt.outFormat,
|
||
|
file: PATH.relative(process.cwd(), f)
|
||
|
});
|
||
|
if (targInfo.fmt.files && targInfo.fmt.files.length) {
|
||
|
theFormat = _fmts.filter(function(fmt) {
|
||
|
return fmt.name === targInfo.fmt.outFormat;
|
||
|
})[0];
|
||
|
MKDIRP.sync(PATH.dirname(f));
|
||
|
_opts.targets = finished;
|
||
|
ret = theFormat.gen.generate(_rezObj, f, _opts);
|
||
|
} else {
|
||
|
theFormat = _fmts.filter(function(fmt) {
|
||
|
return fmt.name === targInfo.fmt.outFormat;
|
||
|
})[0];
|
||
|
outFolder = PATH.dirname(f);
|
||
|
MKDIRP.sync(outFolder);
|
||
|
ret = theFormat.gen.generate(_rezObj, f, _opts);
|
||
|
}
|
||
|
} catch (_error) {
|
||
|
e = _error;
|
||
|
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 = function(targets, theme) {
|
||
|
this.stat(HMEVENT.verifyOutputs, {
|
||
|
targets: targets,
|
||
|
theme: theme
|
||
|
});
|
||
|
return _.reject(targets.map(function(t) {
|
||
|
var pathInfo;
|
||
|
pathInfo = parsePath(t);
|
||
|
return {
|
||
|
format: pathInfo.extname.substr(1)
|
||
|
};
|
||
|
}), function(t) {
|
||
|
return 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 = function(theTheme) {
|
||
|
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
|
||
|
};
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
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 = function(dst, theTheme) {
|
||
|
var destColl, targets;
|
||
|
destColl = (dst && dst.length && dst) || [PATH.normalize('out/resume.all')];
|
||
|
targets = [];
|
||
|
destColl.forEach(function(t) {
|
||
|
var fmat, pa, to;
|
||
|
to = PATH.resolve(t);
|
||
|
pa = parsePath(to);
|
||
|
fmat = pa.extname || '.all';
|
||
|
return targets.push.apply(targets, fmat === '.all' ? Object.keys(theTheme.formats).map(function(k) {
|
||
|
var z;
|
||
|
z = theTheme.formats[k];
|
||
|
return {
|
||
|
file: to.replace(/all$/g, z.outFormat),
|
||
|
fmt: z
|
||
|
};
|
||
|
}) : [
|
||
|
{
|
||
|
file: to,
|
||
|
fmt: theTheme.getFormat(fmat.slice(1))
|
||
|
}
|
||
|
]);
|
||
|
});
|
||
|
return targets;
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
Verify the specified theme name/path.
|
||
|
*/
|
||
|
|
||
|
verifyTheme = function(themeNameOrPath) {
|
||
|
var exists, tFolder;
|
||
|
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
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
return tFolder;
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
Load the specified theme, which could be either a FRESH theme or a JSON Resume
|
||
|
theme.
|
||
|
*/
|
||
|
|
||
|
loadTheme = function(tFolder) {
|
||
|
var theTheme;
|
||
|
theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ? new JRSTheme().open(tFolder) : new FRESHTheme().open(tFolder);
|
||
|
_opts.themeObj = theTheme;
|
||
|
return theTheme;
|
||
|
};
|
||
|
|
||
|
}).call(this);
|