diff --git a/package.json b/package.json index bef8d59..4b22adb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hackmyresume", - "version": "1.3.0-beta", + "version": "1.3.0", "description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.", "repository": { "type": "git", @@ -47,12 +47,13 @@ "dependencies": { "colors": "^1.1.2", "copy": "^0.1.3", - "fresh-themes": "~0.9.3-beta", "fresca": "~0.2.2", + "fresh-themes": "~0.9.3-beta", "fs-extra": "^0.24.0", "handlebars": "^4.0.5", "html": "0.0.10", "is-my-json-valid": "^2.12.2", + "json-lint": "^0.1.0", "jst": "0.0.13", "lodash": "^3.10.1", "marked": "^0.3.5", diff --git a/src/core/convert.js b/src/core/convert.js index 29f1a47..b579cf9 100644 --- a/src/core/convert.js +++ b/src/core/convert.js @@ -1,11 +1,13 @@ /** FRESH to JSON Resume conversion routiens. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@license MIT. See LICENSE.md for details. @module convert.js */ (function(){ + var _ = require('underscore'); + /** Convert between FRESH and JRS resume/CV formats. @class FRESHConverter @@ -26,6 +28,8 @@ FRESH to JSON Resume conversion routiens. name: src.basics.name, + imp: src.basics.imp, + info: { label: src.basics.label, class: src.basics.class, // <--> round-trip @@ -92,7 +96,8 @@ FRESH to JSON Resume conversion routiens. countryCode: src.location.country, region: src.location.region }, - profiles: social( src.social, false ) + profiles: social( src.social, false ), + imp: src.imp }, work: employment( src.employment, false ), @@ -109,12 +114,30 @@ FRESH to JSON Resume conversion routiens. }; + }, + + toSTRING: function( src ) { + function replacerJRS( key,value ) { // Exclude these keys from stringification + return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', + 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', + 'isModified', 'htmlPreview', 'display_progress_bar'], + function( val ) { return key.trim() === val; } + ) ? undefined : value; + } + function replacerFRESH( key,value ) { // Exclude these keys from stringification + return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', + 'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'], + function( val ) { return key.trim() === val; } + ) ? undefined : value; + } + + return JSON.stringify( src, src.basics ? replacerJRS : replacerFRESH, 2 ); } }; function meta( direction, obj ) { - if( !obj ) return obj; // preserve null and undefined + //if( !obj ) return obj; // preserve null and undefined if( direction ) { obj = obj || { }; obj.format = obj.format || "FRESH@0.1.0"; @@ -151,7 +174,7 @@ FRESH to JSON Resume conversion routiens. start: job.startDate, end: job.endDate, url: job.website, - keywords: "", + keywords: [], highlights: job.highlights }; }) : undefined @@ -164,6 +187,7 @@ FRESH to JSON Resume conversion routiens. if( !obj ) return obj; if( direction ) { return obj && obj.length ? { + level: "", history: obj.map(function(edu){ return { institution: edu.institution, @@ -171,8 +195,8 @@ FRESH to JSON Resume conversion routiens. end: edu.endDate, grade: edu.gpa, curriculum: edu.courses, - url: edu.website || edu.url || null, - summary: null, + url: edu.website || edu.url || undefined, + summary: edu.summary || "", area: edu.area, studyType: edu.studyType }; diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index 58c97a5..54d08ac 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -13,6 +13,7 @@ Definition of the FRESHResume class. , __ = require('lodash') , PATH = require('path') , moment = require('moment') + , XML = require('xml-escape') , MD = require('marked') , CONVERTER = require('./convert') , JRSResume = require('./jrs-resume'); @@ -81,12 +82,90 @@ Definition of the FRESHResume class. return JSON.stringify( obj, replacer, 2 ); }; + /** + Create a copy of this resume in which all string fields have been run through + a transformation function (such as a Markdown filter or XML encoder). + */ + FreshResume.prototype.transformStrings = function( filters, transformer ) { + + var that = this; + var ret = this.dupe(); + + // TODO: refactor recursion + function transformStringsInObject( obj ) { + + if( !obj ) return; + if( moment.isMoment( obj ) ) return; + + if( _.isArray( obj ) ) { + obj.forEach( function(elem, idx, ar) { + if( typeof elem === 'string' || elem instanceof String ) + ar[idx] = transformer( null, elem ); + else if (_.isObject(elem)) + transformStringsInObject( elem ); + }); + } + else if (_.isObject( obj )) { + Object.keys( obj ).forEach(function(k) { + var sub = obj[k]; + if( typeof sub === 'string' || sub instanceof String ) { + if( filters.length && _.contains(filters, k) ) + return; + obj[k] = transformer( k, sub ); + } + else if (_.isObject( sub )) + transformStringsInObject( sub ); + }); + } + + } + + Object.keys( ret ).forEach(function(member){ + transformStringsInObject( ret[ member ] ); + }); + + return ret; + }; + /** Create a copy of this resume in which all fields have been interpreted as Markdown. */ FreshResume.prototype.markdownify = function() { + function MDIN( txt ){ + return MD(txt || '' ).replace(/^\s*
|<\/p>\s*$/gi, ''); + } + + function trx(key, val) { + if( key === 'summary' ) { + return MD(val); + } + return MDIN(val); + } + + return this.transformStrings( ['skills','url','start','end','date'], trx ); + }; + + /** + Create a copy of this resume in which all fields have been interpreted as + Markdown. + */ + FreshResume.prototype.xmlify = function() { + + function trx(key, val) { + return XML(val); + } + + return this.transformStrings( [], trx ); + }; + + /** + Create a copy of this resume in which all fields have been interpreted as + Markdown. + */ + FreshResume.prototype.markdownify2 = function() { + var that = this; var ret = this.dupe(); diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js index 49a8d86..5609a75 100644 --- a/src/core/jrs-resume.js +++ b/src/core/jrs-resume.js @@ -1,6 +1,6 @@ /** Definition of the JRSResume class. -@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. +@license MIT. See LICENSE.md for details. @module jrs-resume.js */ @@ -12,6 +12,7 @@ Definition of the JRSResume class. , _ = require('underscore') , PATH = require('path') , MD = require('marked') + , CONVERTER = require('./convert') , moment = require('moment'); /** @@ -51,6 +52,24 @@ Definition of the JRSResume class. return this; }; + /** + Save the sheet to disk in a specific format, either FRESH or JRS. + */ + JRSResume.prototype.saveAs = function( filename, format ) { + + if( format === 'JRS' ) { + this.basics.imp.fileName = filename || this.imp.fileName; + FS.writeFileSync( this.basics.imp.fileName, this.stringify(), 'utf8' ); + } + else { + var newRep = CONVERTER.toFRESH( this ); + var stringRep = CONVERTER.toSTRING( newRep ); + FS.writeFileSync( filename, stringRep, 'utf8' ); + } + return this; + + }; + /** Convert this object to a JSON string, sanitizing meta-properties along the way. Don't override .toString(). @@ -92,6 +111,7 @@ Definition of the JRSResume class. if( opts.imp === undefined || opts.imp ) { this.basics.imp = this.basics.imp || { }; this.basics.imp.title = (opts.title || this.basics.imp.title) || this.basics.name; + this.basics.imp.orgFormat = 'JRS'; } // Parse dates, sort dates, and calculate computed values (opts.date === undefined || opts.date) && _parseDates.call( this ); diff --git a/src/core/resume-factory.js b/src/core/resume-factory.js index b72f8a9..b8d4265 100644 --- a/src/core/resume-factory.js +++ b/src/core/resume-factory.js @@ -1,43 +1,107 @@ /** -Core resume-loading logic for HackMyResume. +Definition of the ResumeFactory class. +@license MIT. See LICENSE.md for details. @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 = { + var ResumeFactory = module.exports = { + + /** - Load one or more resumes in a specific source format. + Load one or more resumes from disk. */ - 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 ); + load: function ( sources, log, toFormat, objectify ) { + // Loop over all inputs, parsing each to JSON and then to a FRESHResume + // or JRSResume object. + var that = this; + return sources.map( function( src ) { + return that.loadOne( src, log, toFormat, objectify ); }); - } + }, + + + /** + Load a single resume from disk. + */ + loadOne: function( src, log, toFormat, objectify ) { + + // Get the destination format. Can be 'fresh', 'jrs', or null/undefined. + toFormat && (toFormat = toFormat.toLowerCase().trim()); + + // Load and parse the resume JSON + var info = _parse( src, log, toFormat ); + if( info.error ) return info; + var json = info.json; + + // Determine the resume format: FRESH or JRS + var orgFormat = ( json.meta && json.meta.format && + json.meta.format.startsWith('FRESH@') ) ? + 'fresh' : 'jrs'; + + // Convert between formats if necessary + if( toFormat && (orgFormat !== toFormat) ) { + json = ResumeConverter[ 'to' + toFormat.toUpperCase() ]( json ); + } + + // Objectify the resume, that is, convert it from JSON to a FRESHResume + // or JRSResume object. + var rez; + if( objectify ) { + var ResumeClass = require('../core/' + (toFormat || orgFormat) + '-resume'); + rez = new ResumeClass().parseJSON( json ); + } + + return { + file: src, + json: info.json, + rez: rez + }; + } }; + + + function _parse( fileName, log, toFormat ) { + var rawData; + try { + + // TODO: Core should not log + log( 'Reading '.info + /*orgFormat.toUpperCase().infoBold +*/ + 'resume: '.info + fileName.cyan.bold ); + + rawData = FS.readFileSync( fileName, 'utf8' ); + return { + json: JSON.parse( rawData ) + }; + + } + catch(ex) { + return { + error: ex, + raw: rawData + }; + } + } + + + }()); diff --git a/src/eng/generic-helpers.js b/src/eng/generic-helpers.js index a930933..6b10dc8 100644 --- a/src/eng/generic-helpers.js +++ b/src/eng/generic-helpers.js @@ -9,6 +9,7 @@ Generic template helper definitions for HackMyResume / FluentCV. var MD = require('marked') , H2W = require('../utils/html-to-wpml') + , XML = require('xml-escape') , moment = require('moment') , _ = require('underscore'); @@ -33,10 +34,12 @@ Generic template helper definitions for HackMyResume / FluentCV. wpml: function( txt, inline ) { if(!txt) return ''; inline = (inline && !inline.hash) || false; + txt = XML(txt.trim()); txt = inline ? - MD(txt.trim()).replace(/^\s*
|<\/p>\s*$/gi, '') : - MD(txt.trim()); - txt = H2W( txt.trim() ); + MD(txt).replace(/^\s*
|<\/p>\s*$/gi, '') :
+ MD(txt);
+ txt = H2W( txt );
+ console.log(txt);
return txt;
},
diff --git a/src/eng/handlebars-generator.js b/src/eng/handlebars-generator.js
index 27a8f7d..b9895eb 100644
--- a/src/eng/handlebars-generator.js
+++ b/src/eng/handlebars-generator.js
@@ -35,8 +35,13 @@ Definition of the HandlebarsGenerator class.
// Compile and run the Handlebars template.
var template = HANDLEBARS.compile(jst);
+
+ var encData = json;
+ ( format === 'html' || format === 'pdf' ) && (encData = json.markdownify());
+ ( format === 'doc' ) && (encData = json.xmlify());
+
return template({
- r: format === 'html' || format === 'pdf' || format === 'png' ? json.markdownify() : json,
+ r: encData,
RAW: json,
filt: opts.filters,
cssInfo: cssInfo,
diff --git a/src/index.js b/src/index.js
index 9352d49..904d790 100644
--- a/src/index.js
+++ b/src/index.js
@@ -73,7 +73,7 @@ function main() {
// Massage inputs and outputs
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 );
- ( splitAt === -1 ) && src.length > 1 && dst.push( src.pop() ); // Allow omitting TO keyword
+ ( splitAt === -1 ) && (src.length > 1) && (verb !== 'validate') && dst.push( src.pop() ); // Allow omitting TO keyword
// Invoke the action
(FCMD.verbs[verb] || FCMD.alias[verb]).apply(null, [src, dst, opts, logMsg]);
diff --git a/src/utils/html-to-wpml.js b/src/utils/html-to-wpml.js
index 89f1c51..243a18d 100644
--- a/src/utils/html-to-wpml.js
+++ b/src/utils/html-to-wpml.js
@@ -47,6 +47,7 @@ Definition of the Markdown to WordProcessingML conversion routine.
break;
case 'Chars':
+ if( tok.chars.trim().length ) {
var style = is_bold ? '