diff --git a/package.json b/package.json index fceeae0..ba19457 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "path-exists": "^2.1.0", "recursive-readdir-sync": "^1.0.6", "simple-html-tokenizer": "^0.2.0", + "string.prototype.startswith": "^0.2.0", "underscore": "^1.8.3", "wkhtmltopdf": "^0.1.5", "xml-escape": "^1.0.0", diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index 4754194..58c97a5 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -143,15 +143,12 @@ Definition of the FRESHResume class. }; /** - Open and parse the specified JSON resume sheet. Merge the JSON object model - onto this Sheet instance with extend() and convert sheet dates to a safe & + Initialize the FreshResume from JSON data. + Open and parse the specified FRESH resume. Merge the JSON object model onto + this Sheet instance with extend() and convert sheet dates to a safe & consistent format. Then sort each section by startDate descending. */ - FreshResume.prototype.parse = function( stringData, opts ) { - - // Parse the incoming JSON representation - var rep = JSON.parse( stringData ); - + FreshResume.prototype.parseJSON = function( rep, opts ) { // Convert JSON Resume to FRESH if necessary if( rep.basics ) { rep = CONVERTER.toFRESH( rep ); @@ -178,6 +175,13 @@ Definition of the FRESHResume class. return this; }; + /** + Initialize the the FreshResume from string data. + */ + FreshResume.prototype.parse = function( stringData, opts ) { + return this.parseJSON( JSON.parse( stringData ), opts ); + }; + /** Return a unique list of all keywords across all skills. */ diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js index c9311a0..619e83a 100644 --- a/src/core/jrs-resume.js +++ b/src/core/jrs-resume.js @@ -70,6 +70,7 @@ Definition of the JRSResume class. }; /** + Initialize the JRS Resume from string data. Open and parse the specified JSON resume sheet. Merge the JSON object model onto this Sheet instance with extend() and convert sheet dates to a safe & consistent format. Then sort each section by startDate descending. @@ -77,7 +78,14 @@ Definition of the JRSResume class. JRSResume.prototype.parse = function( stringData, opts ) { opts = opts || { }; var rep = JSON.parse( stringData ); + return this.parseJSON( rep, opts ); + }; + /** + Initialize the JRSRume from JSON data. + */ + JRSResume.prototype.parseJSON = function( rep, opts ) { + opts = opts || { }; extend( true, this, rep ); // Set up metadata if( opts.imp === undefined || opts.imp ) { diff --git a/src/core/load-source-resumes.js b/src/core/load-source-resumes.js deleted file mode 100644 index b26ae70..0000000 --- a/src/core/load-source-resumes.js +++ /dev/null @@ -1,13 +0,0 @@ -(function(){ - - var FRESHResume = require('../core/fresh-resume'); - - module.exports = function loadSourceResumes( src, log, fn ) { - return src.map( function( res ) { - log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info + - res.cyan.bold ); - return (fn && fn(res)) || (new FRESHResume()).open( res ); - }); - }; - -}()); diff --git a/src/core/resume-factory.js b/src/core/resume-factory.js new file mode 100644 index 0000000..b72f8a9 --- /dev/null +++ b/src/core/resume-factory.js @@ -0,0 +1,43 @@ +/** +Core resume-loading logic for HackMyResume. +@module resume-factory.js +*/ + +(function(){ + + require('string.prototype.startswith'); + var FS = require('fs'); + var ResumeConverter = require('./convert'); + + /** + A simple factory class for FRESH and JSON Resumes. + @class ResumeFactory + */ + module.exports = { + + /** + Load one or more resumes in a specific source format. + */ + load: function ( src, log, fn, toFormat ) { + + toFormat = toFormat && (toFormat.toLowerCase().trim()) || 'fresh'; + var ResumeClass = require('../core/' + toFormat + '-resume'); + + return src.map( function( res ) { + var rezJson = JSON.parse( FS.readFileSync( res ) ); + var orgFormat = ( rezJson.meta && rezJson.meta.format && + rezJson.meta.format.startsWith('FRESH@') ) ? + 'fresh' : 'jrs'; + if(orgFormat !== toFormat) { + rezJson = ResumeConverter[ 'to' + toFormat.toUpperCase() ]( rezJson ); + } + // TODO: Core should not log + log( 'Reading '.info + orgFormat.toUpperCase().infoBold + ' resume: '.info + res.cyan.bold ); + return (fn && fn(res)) || (new ResumeClass()).parseJSON( rezJson ); + }); + + } + + }; + +}()); diff --git a/src/core/theme.js b/src/core/theme.js index 3b5f729..6e8356a 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -1,6 +1,6 @@ /** Definition of the Theme class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@license MIT. Copyright (c) 2015 hacksalot / FluentDesk. @module theme.js */ @@ -12,6 +12,7 @@ Definition of the Theme class. , _ = require('underscore') , PATH = require('path') , parsePath = require('parse-filepath') + , pathExists = require('path-exists').sync , EXTEND = require('../utils/extend') , moment = require('moment') , RECURSIVE_READ_DIR = require('recursive-readdir-sync'); @@ -29,9 +30,26 @@ Definition of the Theme class. */ Theme.prototype.open = function( themeFolder ) { - // Open the [theme-name].json file; should have the same name as folder this.folder = themeFolder; + + // Open the [theme-name].json file; should have the same name as folder var pathInfo = parsePath( themeFolder ); + + // Set up a formats hash for the theme + var formatsHash = { }; + + // See if the theme has a package.json. If so, load it. + var packageJsonPath = PATH.join(themeFolder, 'package.json'); + if( pathExists( packageJsonPath ) ) { + var themePack = require( themeFolder ); + var themePkgJson = require( packageJsonPath ); + this.name = themePkgJson.name; + this.render = (themePack && themePack.render) || undefined; + this.formats = { html: { title: 'html', outFormat: 'html', ext: 'html', path: null, data: null } }; + return this; + } + + // Otherwise, do a full theme load var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' ); var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) ); var that = this; @@ -39,9 +57,6 @@ Definition of the Theme class. // Move properties from the theme JSON file to the theme object EXTEND( true, this, themeInfo ); - // Set up a formats has for the theme - var formatsHash = { }; - // Check for an explicit "formats" entry in the theme JSON. If it has one, // then this theme declares its files explicitly. if( !!this.formats ) { diff --git a/src/verbs/convert.js b/src/verbs/convert.js index b6c1239..7386072 100644 --- a/src/verbs/convert.js +++ b/src/verbs/convert.js @@ -1,6 +1,6 @@ (function(){ - var loadSourceResumes = require('../core/load-source-resumes'); + var ResumeFactory = require('../core/resume-factory'); /** Convert between FRESH and JRS formats. @@ -16,7 +16,7 @@ if( src && dst && src.length && dst.length && src.length !== dst.length ) { throw { fluenterror: 7 }; } - var sheets = loadSourceResumes( src, _log ); + var sheets = ResumeFactory.load( src, _log ); sheets.forEach(function(sheet, idx){ var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; diff --git a/src/verbs/generate.js b/src/verbs/generate.js index d6629d2..0061ab2 100644 --- a/src/verbs/generate.js +++ b/src/verbs/generate.js @@ -1,11 +1,12 @@ (function() { var PATH = require('path') + , FS = require('fs') , parsePath = require('parse-filepath') , MKDIRP = require('mkdirp') , _opts = require('../core/default-options') , FluentTheme = require('../core/theme') - , loadSourceResumes = require('../core/load-source-resumes') + , ResumeFactory = require('../core/resume-factory') , _ = require('underscore') , _fmts = require('../core/default-formats') , _err, _log, rez; @@ -36,9 +37,27 @@ _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; _opts.prettify = opts.prettify === true ? _opts.prettify : false; + // Verify the specified theme name/path + var relativeThemeFolder = '../../node_modules/fluent-themes/themes'; + var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme); + var exists = require('path-exists').sync; + if( !exists( tFolder ) ) { + tFolder = PATH.resolve( _opts.theme ); + if (!exists( tFolder )) { + throw { fluenterror: 1, data: _opts.theme }; + } + } + + // Load the theme + var theTheme = (new FluentTheme()).open( tFolder ); + _opts.themeObj = theTheme; + var numFormats = theTheme.formats ? Object.keys(theTheme.formats).length : 2; + _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + + (' theme (' + numFormats + ' formats)').info); + // Load input resumes... if( !src || !src.length ) { throw { fluenterror: 3 }; } - var sheets = loadSourceResumes( src, _log ); + var sheets = ResumeFactory.load( src, _log, null, theTheme.render ? 'JRS' : 'FRESH' ); // Merge input resumes... var msg = ''; @@ -49,23 +68,6 @@ }); msg && _log(msg); - // Verify the specified theme name/path - var relativeThemeFolder = '../../node_modules/fluent-themes/themes'; - var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme); - var exists = require('path-exists').sync; - if (!exists( tFolder )) { - tFolder = PATH.resolve( _opts.theme ); - if (!exists( tFolder )) { - throw { fluenterror: 1, data: _opts.theme }; - } - } - - // Load the theme - var theTheme = (new FluentTheme()).open( tFolder ); - _opts.themeObj = theTheme; - _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + - (' theme (' + Object.keys(theTheme.formats).length + ' formats)').info); - // Expand output resumes... (can't use map() here) var targets = [], that = this; ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { @@ -74,11 +76,15 @@ pa = parsePath(to), fmat = pa.extname || '.all'; - targets.push.apply(targets, fmat === '.all' ? + targets.push.apply( + targets, fmat === '.all' ? + Object.keys( theTheme.formats ).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) ) }]); + }) : + + [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); }); @@ -113,22 +119,6 @@ function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; theFormat.gen.generate( rez, f, _opts ); - - // targInfo.fmt.files.forEach( function( form ) { - // - // if( form.action === 'transform' ) { - // var theFormat = _fmts.filter( function( fmt ) { - // return fmt.name === targInfo.fmt.outFormat; - // })[0]; - // MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; - // theFormat.gen.generate( rez, f, _opts ); - // } - // else if( form.action === null ) { - // // Copy the file - // } - // - // }); - } // Otherwise the theme has no files section else { @@ -139,7 +129,14 @@ theFormat = _fmts.filter( function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists; - theFormat.gen.generate( rez, f, _opts ); + + if( theme.render ) { + var rezHtml = theme.render( rez ); + FS.writeFileSync( f, rezHtml ); + } + else { + theFormat.gen.generate( rez, f, _opts ); + } } } catch( ex ) { diff --git a/src/verbs/validate.js b/src/verbs/validate.js index 52bd6da..239576c 100644 --- a/src/verbs/validate.js +++ b/src/verbs/validate.js @@ -1,7 +1,7 @@ (function() { var FS = require('fs'); - var loadSourceResumes = require('../core/load-source-resumes'); + var ResumeFactory = require('../core/resume-factory'); module.exports = @@ -20,7 +20,7 @@ }; // Load input resumes... - var sheets = loadSourceResumes(src, _log, function( res ) { + var sheets = ResumeFactory.load(src, _log, function( res ) { try { return { file: res,