-
Notifications
You must be signed in to change notification settings - Fork 526
/
build.coffee
359 lines (294 loc) · 10.1 KB
/
build.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
###*
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 }
, @
# 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
newEx =
fluenterror: HMSTATUS.themeLoad
inner: _error
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 );
# 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.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
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 ) ->
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
return fluenterror: 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