/** 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') , _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... _log = logger || console.log; _err = errHandler || error; //_opts = extend( true, _opts, opts ); _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; _opts.prettify = opts.prettify === true ? _opts.prettify : false; _opts.css = opts.css; // Load the theme... 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, theme.render ? 'JRS' : 'FRESH', true); // Merge input resumes... var msg = ''; var rezRep = _.reduceRight( sheets, function( a, b, idx ) { msg += ((idx == sheets.length - 2) ? 'Merging '.gray + a.rez.imp.fileName : '') + ' onto '.gray + b.rez.fileName; return extend( true, b.rez, a.rez ); }); rez = rezRep.rez; msg && _log(msg); // Expand output resumes... var targets = expand( dst, theme ); // Run the transformation! targets.forEach( function(t) { t.final = single( t, theme, targets ); }); // 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; _log( 'Generating '.useful + targInfo.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + PATH.relative(process.cwd(), f ).useful.bold ); // 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( /@@@@~.+?~@@@@/g, 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.join( parsePath ( require.resolve('fresh-themes') ).dirname, 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; // Output a message TODO: core should not log var numFormats = Object.keys(theTheme.formats).length; _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + (' theme (' + numFormats + ' formats)').info); return theTheme; } module.exports = build; }());