/** Implementation of the 'generate' verb for HackMyResume. @module generate.js @license MIT. See LICENSE.md for details. */ (function() { var PATH = require('path') , FS = require('fs') , MD = require('marked') , MKDIRP = require('mkdirp') , EXTEND = require('../utils/extend') , parsePath = require('parse-filepath') , _opts = require('../core/default-options') , FluentTheme = require('../core/fresh-theme') , JRSTheme = require('../core/jrs-theme') , ResumeFactory = require('../core/resume-factory') , _ = require('underscore') , _fmts = require('../core/default-formats') , extend = require('../utils/extend') , chalk = require('chalk') , pad = require('string-padding') , _err, _log, rez; /** Handle an exception. */ function error( ex ) { throw ex; } /** 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. */ function build( src, dst, opts, logger, errHandler ) { // Housekeeping //_opts = extend( true, _opts, opts ); _log = logger || console.log; _err = errHandler || error; _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; _opts.prettify = opts.prettify === true ? _opts.prettify : false; _opts.css = opts.css; _opts.pdf = opts.pdf; _opts.wrap = opts.wrap || 60; // If two or more files are passed to the GENERATE command and the TO // keyword is omitted, the last file specifies the output file. if( src.length > 1 && ( !dst || !dst.length ) ) { dst.push( src.pop() ); } // Load the theme...we do this first because the theme choice (FRESH or // JSON Resume) determines what format we'll convert the resume to. var tFolder = verify_theme( _opts.theme ); var theme = load_theme( tFolder ); // Load input resumes... if( !src || !src.length ) { throw { fluenterror: 3 }; } var sheets = ResumeFactory.load(src, { log: _log, format: theme.render ? 'JRS' : 'FRESH', objectify: true, throw: true }).map(function(sh){ return sh.rez; }); // Merge input resumes... var msg = ''; rez = _.reduceRight( sheets, function( a, b, idx ) { msg += ((idx == sheets.length - 2) ? chalk.cyan('Merging ') + chalk.cyan.bold(a.imp().file) : '') + chalk.cyan(' onto ') + chalk.cyan.bold(b.imp().file); return extend( true, b, a ); }); msg && _log(msg); // Output theme messages var numFormats = Object.keys(theme.formats).length; var themeName = theme.name.toUpperCase(); _log( chalk.yellow('Applying ') + chalk.yellow.bold(themeName) + chalk.yellow(' theme (' + numFormats + ' format' + ( numFormats === 1 ? ')' : 's)') )); // Expand output resumes... var targets = expand( dst, theme ); // Run the transformation! targets.forEach( function(t) { t.final = single( t, theme, targets ); }); if( theme.message ) { var WRAP = require('word-wrap'); _log( WRAP( chalk.gray('The ' + themeName + ' theme says: "') + chalk.white(theme.message) + chalk.gray('"'), { width: _opts.wrap, indent: '' } )); } // Don't send the client back empty-handed return { sheet: rez, targets: targets, processed: targets }; } /** Generate a single target resume such as "out/rez.html" or "out/rez.doc". @param targInfo Information for the target resume. @param theme A FRESHTheme or JRSTheme object. @returns */ function single( targInfo, theme, finished ) { function MDIN(txt) { // TODO: Move this return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); } try { var f = targInfo.file , fType = targInfo.fmt.outFormat , fName = PATH.basename(f, '.' + fType) , theFormat; var suffix = ''; if( targInfo.fmt.outFormat === 'pdf' && _opts.pdf ) { suffix = chalk.green(' (with ' + _opts.pdf + ')'); } _log( chalk.green('Generating ') + chalk.green.bold(pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) + chalk.green(' resume') + suffix + chalk.green(': ') + chalk.green.bold( 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( function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; _opts.targets = finished; return theFormat.gen.generate( rez, f, _opts ); } // Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format // (JSON, YML, or PNG) that every theme gets "for free". else { theFormat = _fmts.filter( function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; var outFolder = PATH.dirname( f ); MKDIRP.sync( outFolder ); // Ensure dest folder exists; // JSON Resume themes have a 'render' method that needs to be called if( theme.render ) { var COPY = require('copy'); var globs = [ '*.css', '*.js', '*.png', '*.jpg', '*.gif', '*.bmp' ]; COPY.sync( globs , outFolder, { cwd: theme.folder, nodir: true, ignore: ['node_modules/','node_modules/**'] // rewrite: function(p1, p2) { // return PATH.join(p2, p1); // } }); // Prevent JSON Resume theme .js from chattering (TODO: redirect IO) var consoleLog = console.log; console.log = function() { }; // Call the theme's render method var rezDupe = rez.harden(); var rezHtml = theme.render( rezDupe ); // Turn logging back on console.log = consoleLog; // Unharden rezHtml = rezHtml.replace( /@@@@~.*?~@@@@/gm, function(val){ return MDIN( val.replace( /~@@@@/gm,'' ).replace( /@@@@~/gm,'' ) ); }); // Save the file FS.writeFileSync( f, rezHtml ); // Return markup to the client return rezHtml; } else { return theFormat.gen.generate( rez, f, _opts ); } } } catch( ex ) { _err( ex ); } } /** 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. */ function expand( dst, 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 }; } // 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. var destColl = (dst && dst.length && dst) || [PATH.normalize('out/resume.all')]; // Assemble an array of expanded target files... (can't use map() here) var targets = []; destColl.forEach( function(t) { var to = PATH.resolve(t), pa = parsePath(to),fmat = pa.extname || '.all'; var explicitFormats = _.omit( theTheme.formats, function(val, key) { return !val.freebie; }); var implicitFormats = _.omit( theTheme.formats, function(val) { return val.freebie; }); targets.push.apply( targets, fmat === '.all' ? Object.keys( implicitFormats ).map( function( k ) { var z = theTheme.formats[k]; return { file: to.replace( /all$/g, z.outFormat ), fmt: z }; }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); targets.push.apply( targets, fmat === '.all' ? Object.keys( explicitFormats ).map( function( k ) { var 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. */ function verify_theme( themeNameOrPath ) { var tFolder = PATH.resolve( __dirname, '../../node_modules/fresh-themes/themes', themeNameOrPath ); var exists = require('path-exists').sync; if( !exists( tFolder ) ) { tFolder = PATH.resolve( themeNameOrPath ); if( !exists( tFolder ) ) { throw { fluenterror: 1, data: _opts.theme }; } } return tFolder; } /** Load the specified theme. */ function load_theme( tFolder ) { // Create a FRESH or JRS theme object var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ? new JRSTheme().open(tFolder) : new FluentTheme().open( tFolder ); // Cache the theme object _opts.themeObj = theTheme; return theTheme; } module.exports = build; }());