diff --git a/package.json b/package.json index a99624c..3dbbf1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fluentcv", - "version": "0.10.1", + "version": "0.10.2", "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", @@ -32,9 +32,9 @@ }, "homepage": "https://github.com/fluentdesk/fluentcv", "dependencies": { - "fresca": "~0.2.1", "colors": "^1.1.2", - "fluent-themes": "~0.6.1-beta", + "fluent-themes": "~0.6.2-beta", + "fresca": "~0.2.1", "fs-extra": "^0.24.0", "handlebars": "^4.0.5", "html": "0.0.10", @@ -45,6 +45,7 @@ "mkdirp": "^0.5.1", "moment": "^2.10.6", "recursive-readdir-sync": "^1.0.6", + "simple-html-tokenizer": "^0.2.0", "underscore": "^1.8.3", "wkhtmltopdf": "^0.1.5", "xml-escape": "^1.0.0", diff --git a/src/core/convert.js b/src/core/convert.js index f16aafc..97c8c5f 100644 --- a/src/core/convert.js +++ b/src/core/convert.js @@ -14,6 +14,7 @@ FRESH to JSON Resume conversion routiens. /** Convert from JSON Resume format to FRESH. + @method toFresh */ toFRESH: function( src, foreign ) { @@ -47,87 +48,15 @@ FRESH to JSON Resume conversion routiens. address: src.basics.location.address }, - employment: { - history: src.work.map( function( job ) { - return { - position: job.position, - employer: job.company, - summary: job.summary, - current: (!job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current') || undefined, - start: job.startDate, - end: job.endDate, - url: job.website, - keywords: "", - highlights: job.highlights - }; - }) - }, - - education: { - history: src.education.map(function(edu){ - return { - institution: edu.institution, - start: edu.startDate, - end: edu.endDate, - grade: edu.gpa, - curriculum: edu.courses, - url: edu.website || edu.url || null, - summary: null, - area: edu.area, - studyType: edu.studyType - }; - }) - }, - - service: { - history: src.volunteer.map(function(vol) { - return { - type: 'volunteer', - position: vol.position, - organization: vol.organization, - start: vol.startDate, - end: vol.endDate, - url: vol.website, - summary: vol.summary, - highlights: vol.highlights - }; - }) - }, - + employment: employment( src.work, true ), + education: education( src.education, true), + service: service( src.volunteer, true), skills: skillsToFRESH( src.skills ), - - writing: src.publications.map(function(pub){ - return { - title: pub.name, - flavor: undefined, - publisher: pub.publisher, - url: pub.website, - date: pub.releaseDate, - summary: pub.summary - }; - }), - - recognition: src.awards.map(function(awd){ - return { - title: awd.title, - date: awd.date, - summary: awd.summary, - from: awd.awarder, - url: null - }; - }), - - social: src.basics.profiles.map(function(pro){ - return { - label: pro.network, - network: pro.network, - url: pro.url, - user: pro.username - }; - }), - + writing: writing( src.publications, true), + recognition: recognition( src.awards, true, foreign ), + social: social( src.basics.profiles, true ), interests: src.interests, - references: src.references, + testimonials: references( src.references, true ), languages: src.languages, disposition: src.disposition // <--> round-trip }; @@ -160,77 +89,17 @@ FRESH to JSON Resume conversion routiens. countryCode: src.location.country, region: src.location.region }, - profiles: src.social.map(function(soc){ - return { - network: soc.network, - username: soc.user, - url: soc.url - }; - }) + profiles: social( src.social, false ) }, - work: src.employment.history.map(function(emp){ - return { - company: emp.employer, - website: emp.url, - position: emp.position, - startDate: emp.start, - endDate: emp.end, - summary: emp.summary, - highlights: emp.highlights - }; - }), - - education: src.education.history.map(function(edu){ - return { - institution: edu.institution, - gpa: edu.grade, - courses: edu.curriculum, - startDate: edu.start, - endDate: edu.end, - area: edu.area, - studyType: edu.studyType - }; - }), - - skills: skillsToJRS( src.skills ), - - volunteer: src.service.history.map(function(srv){ - return { - flavor: foreign ? srv.flavor : undefined, - organization: srv.organization, - position: srv.position, - startDate: srv.start, - endDate: srv.end, - website: srv.url, - summary: srv.summary, - highlights: srv.highlights - }; - }), - - awards: src.recognition.map(function(awd){ - return { - flavor: foreign ? awd.flavor : undefined, - url: foreign ? awd.url: undefined, - title: awd.title, - date: awd.date, - awarder: awd.from, - summary: awd.summary - }; - }), - - publications: src.writing.map(function(pub){ - return { - name: pub.title, - publisher: pub.publisher, - releaseDate: pub.date, - website: pub.url, - summary: pub.summary - }; - }), - + work: employment( src.employment, false ), + education: education( src.education, false ), + skills: skillsToJRS( src.skills, false ), + volunteer: service( src.service, false ), + awards: recognition( src.recognition, false, foreign ), + publications: writing( src.writing, false ), interests: src.interests, - references: src.references, + references: references( src.testimonials, false ), samples: foreign ? src.samples : undefined, disposition: foreign ? src.disposition : undefined, languages: src.languages @@ -250,6 +119,207 @@ FRESH to JSON Resume conversion routiens. return obj; } + function employment( obj, direction ) { + if( !direction ) { + return obj && obj.history ? + obj.history.map(function(emp){ + return { + company: emp.employer, + website: emp.url, + position: emp.position, + startDate: emp.start, + endDate: emp.end, + summary: emp.summary, + highlights: emp.highlights + }; + }) : undefined; + } + else { + return { + history: obj && obj.length ? + obj.map( function( job ) { + return { + position: job.position, + employer: job.company, + summary: job.summary, + current: (!job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current') || undefined, + start: job.startDate, + end: job.endDate, + url: job.website, + keywords: "", + highlights: job.highlights + }; + }) : undefined + }; + } + } + + + function education( obj, direction ) { + if( direction ) { + return obj && obj.length ? { + history: obj.map(function(edu){ + return { + institution: edu.institution, + start: edu.startDate, + end: edu.endDate, + grade: edu.gpa, + curriculum: edu.courses, + url: edu.website || edu.url || null, + summary: null, + area: edu.area, + studyType: edu.studyType + }; + }) + } : undefined; + } + else { + return obj && obj.history ? + obj.history.map(function(edu){ + return { + institution: edu.institution, + gpa: edu.grade, + courses: edu.curriculum, + startDate: edu.start, + endDate: edu.end, + area: edu.area, + studyType: edu.studyType + }; + }) : undefined; + } + } + + function service( obj, direction, foreign ) { + if( direction ) { + return { + history: obj && obj.length ? obj.map(function(vol) { + return { + type: 'volunteer', + position: vol.position, + organization: vol.organization, + start: vol.startDate, + end: vol.endDate, + url: vol.website, + summary: vol.summary, + highlights: vol.highlights + }; + }) : undefined + }; + } + else { + return obj && obj.history ? + obj.history.map(function(srv){ + return { + flavor: foreign ? srv.flavor : undefined, + organization: srv.organization, + position: srv.position, + startDate: srv.start, + endDate: srv.end, + website: srv.url, + summary: srv.summary, + highlights: srv.highlights + }; + }) : undefined; + } + } + + function social( obj, direction ) { + if( direction ) { + return obj.map(function(pro){ + return { + label: pro.network, + network: pro.network, + url: pro.url, + user: pro.username + }; + }); + } + else { + return obj.map( function( soc ) { + return { + network: soc.network, + username: soc.user, + url: soc.url + }; + }); + } + } + + function recognition( obj, direction, foreign ) { + if( direction ) { + return obj && obj.length ? obj.map( + function(awd){ + return { + flavor: foreign ? awd.flavor : undefined, + url: foreign ? awd.url: undefined, + title: awd.title, + date: awd.date, + from: awd.awarder, + summary: awd.summary + }; + }) : undefined; + } + else { + return obj && obj.length ? obj.map(function(awd){ + return { + flavor: foreign ? awd.flavor : undefined, + url: foreign ? awd.url: undefined, + title: awd.title, + date: awd.date, + awarder: awd.from, + summary: awd.summary + }; + }) : undefined; + } + } + + function references( obj, direction ) { + if( direction ) { + return obj && obj.length && obj.map(function(ref){ + return { + name: ref.name, + flavor: 'professional', + quote: ref.reference, + private: false + }; + }); + } + else { + return obj && obj.length && obj.map(function(ref){ + return { + name: ref.name, + reference: ref.quote + }; + }); + } + } + + function writing( obj, direction ) { + if( direction ) { + return obj.map(function( pub ) { + return { + title: pub.name, + flavor: undefined, + publisher: pub.publisher, + url: pub.website, + date: pub.releaseDate, + summary: pub.summary + }; + }); + } + else { + return obj && obj.length ? obj.map(function(pub){ + return { + name: pub.title, + publisher: pub.publisher && pub.publisher.name ? pub.publisher.name : pub.publisher, + releaseDate: pub.date, + website: pub.url, + summary: pub.summary + }; + }) : undefined; + } + } + function skillsToFRESH( skills ) { return { diff --git a/src/core/empty-fresh.json b/src/core/empty-fresh.json index 347def5..356ea50 100644 --- a/src/core/empty-fresh.json +++ b/src/core/empty-fresh.json @@ -47,6 +47,7 @@ "position": "", "summary": "", "start": "", + "end": "", "keywords": [], "highlights": [] } @@ -91,6 +92,7 @@ "sets": [ { "name": "", + "level": "", "skills": [] } ], diff --git a/src/core/fluent-date.js b/src/core/fluent-date.js index 7a3d967..26ba4be 100644 --- a/src/core/fluent-date.js +++ b/src/core/fluent-date.js @@ -66,7 +66,10 @@ FluentDate/*.prototype*/.fmt = function( dt ) { } } else { - if( dt.isValid && dt.isValid() ) + if( !dt ) { + return moment(); + } + else if( dt.isValid && dt.isValid() ) return dt; throw 'Unknown date object encountered.'; } diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index a0f5287..d21449c 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -12,7 +12,8 @@ Definition of the FRESHResume class. , PATH = require('path') , moment = require('moment') , MD = require('marked') - , CONVERTER = require('./convert'); + , CONVERTER = require('./convert') + , JRSResume = require('./jrs-resume'); /** A FRESH-style resume in JSON or YAML. @@ -46,13 +47,14 @@ Definition of the FRESHResume class. Save the sheet to disk in a specific format, either FRESH or JSON Resume. */ FreshResume.prototype.saveAs = function( filename, format ) { - this.imp.fileName = filename || this.imp.fileName; + if( format !== 'JRS' ) { + this.imp.fileName = filename || this.imp.fileName; FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' ); } else { var newRep = CONVERTER.toJRS( this ); - FS.writeFileSync( this.imp.fileName, FreshResume.stringify( newRep ), 'utf8' ); + FS.writeFileSync( filename, JRSResume.stringify( newRep ), 'utf8' ); } return this; }; @@ -211,6 +213,16 @@ Definition of the FRESHResume class. delete this.social; }; + /** + Get a safe count of the number of things in a section. + */ + FreshResume.prototype.count = function( obj ) { + if( !obj ) return 0; + if( obj.history ) return obj.history.length; + if( obj.sets ) return obj.sets.length; + return obj.length || 0; + }; + /** Get the default (empty) sheet. */ @@ -224,9 +236,18 @@ Definition of the FRESHResume class. */ FreshResume.prototype.add = function( moniker ) { var defSheet = FreshResume.default(); - var newObject = $.extend( true, {}, defSheet[ moniker ][0] ); + var newObject = defSheet[moniker].history ? + $.extend( true, {}, defSheet[ moniker ].history[0] ) : + (moniker === 'skills' ? + $.extend( true, {}, defSheet.skills.sets[0] ) : + $.extend( true, {}, defSheet[ moniker ][0] )); this[ moniker ] = this[ moniker ] || []; - this[ moniker ].push( newObject ); + if( this[ moniker ].history ) + this[ moniker ].history.push( newObject ); + else if( moniker === 'skills' ) + this.skills.sets.push( newObject ); + else + this[ moniker ].push( newObject ); return newObject; }; @@ -300,14 +321,15 @@ Definition of the FRESHResume class. */ FreshResume.prototype.duration = function() { if( this.employment.history && this.employment.history.length ) { - var careerStart = this.employment.history[ this.employment.history.length - 1].safe.start; + var firstJob = _.last( this.employment.history ); + var careerStart = firstJob.start ? firstJob.safe.start : ''; if ((typeof careerStart === 'string' || careerStart instanceof String) && !careerStart.trim()) return 0; var careerLast = _.max( this.employment.history, function( w ) { - return w.safe.end.unix(); - }).safe.end; - return careerLast.diff( careerStart, 'years' ); + return( w.safe && w.safe.end ) ? w.safe.end.unix() : moment().unix(); + }); + return careerLast.safe.end.diff( careerStart, 'years' ); } return 0; }; @@ -366,7 +388,7 @@ Definition of the FRESHResume class. replaceDatesInObject( obj[key] ); }); ['start','end','date'].forEach( function(val) { - if( obj[val] && (!obj.safe || !obj.safe[val] )) { + if( (obj[val] !== undefined) && (!obj.safe || !obj.safe[val] )) { obj.safe = obj.safe || { }; obj.safe[ val ] = _fmt( obj[val] ); if( obj[val] && (val === 'start') && !obj.end ) { diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js index 4ec8749..a1c068a 100644 --- a/src/core/jrs-resume.js +++ b/src/core/jrs-resume.js @@ -30,17 +30,22 @@ Definition of the JRSResume class. consistent format. Then sort each section by startDate descending. */ JRSResume.prototype.open = function( file, title ) { - this.imp = { fileName: file }; - this.imp.raw = FS.readFileSync( file, 'utf8' ); - return this.parse( this.imp.raw, title ); + //this.imp = { fileName: file }; <-- schema violation, tuck it into .basics instead + this.basics = { + imp: { + fileName: file, + raw: FS.readFileSync( file, 'utf8' ) + } + }; + return this.parse( this.basics.imp.raw, title ); }; /** Save the sheet to disk (for environments that have disk access). */ JRSResume.prototype.save = function( filename ) { - this.imp.fileName = filename || this.imp.fileName; - FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' ); + this.basics.imp.fileName = filename || this.basics.imp.fileName; + FS.writeFileSync( this.basics.imp.fileName, this.stringify( this ), 'utf8' ); return this; }; @@ -48,7 +53,7 @@ Definition of the JRSResume class. Convert this object to a JSON string, sanitizing meta-properties along the way. Don't override .toString(). */ - JRSResume.prototype.stringify = function() { + JRSResume.stringify = function( obj ) { function replacer( key,value ) { // Exclude these keys from stringification return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', @@ -56,7 +61,11 @@ Definition of the JRSResume class. function( val ) { return key.trim() === val; } ) ? undefined : value; } - return JSON.stringify( this, replacer, 2 ); + return JSON.stringify( obj, replacer, 2 ); + }; + + JRSResume.prototype.stringify = function() { + return JRSResume.stringify( this ); }; /** @@ -67,16 +76,17 @@ Definition of the JRSResume class. JRSResume.prototype.parse = function( stringData, opts ) { opts = opts || { }; var rep = JSON.parse( stringData ); + extend( true, this, rep ); // Set up metadata if( opts.imp === undefined || opts.imp ) { - this.imp = this.imp || { }; - this.imp.title = (opts.title || this.imp.title) || this.basics.name; + this.basics.imp = this.basics.imp || { }; + this.basics.imp.title = (opts.title || this.basics.imp.title) || this.basics.name; } // Parse dates, sort dates, and calculate computed values (opts.date === undefined || opts.date) && _parseDates.call( this ); (opts.sort === undefined || opts.sort) && this.sort(); - (opts.compute === undefined || opts.compute) && (this.computed = { + (opts.compute === undefined || opts.compute) && (this.basics.computed = { numYears: this.duration(), keywords: this.keywords() }); @@ -111,7 +121,7 @@ Definition of the JRSResume class. JRSResume.prototype.clear = function( clearMeta ) { clearMeta = ((clearMeta === undefined) && true) || clearMeta; clearMeta && (delete this.imp); - delete this.computed; // Don't use Object.keys() here + delete this.basics.computed; // Don't use Object.keys() here delete this.work; delete this.volunteer; delete this.education; @@ -169,8 +179,15 @@ Definition of the JRSResume class. var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ), 'utf8' ); var schemaObj = JSON.parse( schema ); var validator = require('is-my-json-valid'); - var validate = validator( schemaObj ); - return validate( this ); + var validate = validator( schemaObj, { // Note [1] + formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } + }); + var ret = validate( this ); + if( !ret ) { + this.basics.imp = this.basics.imp || { }; + this.basics.imp.validationErrors = validate.errors; + } + return ret; }; /** diff --git a/src/core/theme.js b/src/core/theme.js index 3d971a0..cbdc548 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -99,9 +99,18 @@ Abstract theme representation. var outFmt = '', isMajor = false; var portion = pathInfo.dir.replace(tplFolder,''); if( portion && portion.trim() ) { - var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig; + var reg = /^(?:\/|\\)(html|latex|doc|pdf|partials)(?:\/|\\)?/ig; var res = reg.exec( portion ); - res && (outFmt = res[1]); + if( res ) { + if( res[1] !== 'partials' ) { + outFmt = res[1]; + } + else { + that.partials = that.partials || []; + that.partials.push( { name: pathInfo.name, path: absPath } ); + return null; + } + } } // Otherwise, the output format is inferred from the filename, as in @@ -138,7 +147,7 @@ Abstract theme representation. }); // Now, get all the CSS files... - (this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; })) + (this.cssFiles = fmts.filter(function( fmt ){ return fmt && (fmt.ext === 'css'); })) .forEach(function( cssf ) { // For each CSS file, get its corresponding HTML file var idx = _.findIndex(fmts, function( fmt ) { @@ -151,7 +160,7 @@ Abstract theme representation. // Remove CSS files from the formats array fmts = fmts.filter( function( fmt) { - return fmt.ext !== 'css'; + return fmt && (fmt.ext !== 'css'); }); return formatsHash; diff --git a/src/eng/handlebars-generator.js b/src/eng/handlebars-generator.js index 4d04a2a..ef61ed5 100644 --- a/src/eng/handlebars-generator.js +++ b/src/eng/handlebars-generator.js @@ -7,11 +7,66 @@ Handlebars template generate for FluentCV. var _ = require('underscore'); var HANDLEBARS = require('handlebars'); + var FS = require('fs'); + var moment = require('moment'); + var MD = require('marked'); + var H2W = require('../utils/html-to-wpml'); - module.exports = function( json, jst, format, cssInfo, opts ) { + module.exports = function( json, jst, format, cssInfo, opts, theme ) { + + _.each( theme.partials, function( el ) { + var tplData = FS.readFileSync( el.path, 'utf8' ); + var compiledTemplate = HANDLEBARS.compile( tplData ); + HANDLEBARS.registerPartial( el.name, compiledTemplate ); + }); + + HANDLEBARS.registerHelper("formatDate", function(datetime, format) { + if( moment ) { + return moment( datetime ).format( format ); + } + else { + return datetime; + } + }); + + HANDLEBARS.registerHelper("wpml", function( txt, inline ) { + inline = (inline && !inline.hash) || false; + txt = inline ? MD(txt.trim()).replace(/^\s*

|<\/p>\s*$/gi, '') : MD(txt.trim()); + txt = H2W( txt.trim() ); + return txt; + }); + + // http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates/ + HANDLEBARS.registerHelper('compare', function(lvalue, rvalue, options) { + + if (arguments.length < 3) + throw new Error("Handlerbars Helper 'compare' needs 2 parameters"); + + var operator = options.hash.operator || "=="; + var operators = { + '==': function(l,r) { return l == r; }, + '===': function(l,r) { return l === r; }, + '!=': function(l,r) { return l != r; }, + '<': function(l,r) { return l < r; }, + '>': function(l,r) { return l > r; }, + '<=': function(l,r) { return l <= r; }, + '>=': function(l,r) { return l >= r; }, + 'typeof': function(l,r) { return typeof l == r; } + }; + + if (!operators[operator]) + throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator); + var result = operators[operator](lvalue,rvalue); + return result ? options.fn(this) : options.inverse(this); + }); var template = HANDLEBARS.compile(jst); - return template( { r: json, filt: opts.filters, cssInfo: cssInfo, headFragment: opts.headFragment || '' } ); + return template({ + r: json, + filt: opts.filters, + cssInfo: cssInfo, + headFragment: opts.headFragment || '' + }); }; diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js index 54be147..341021c 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -7,7 +7,7 @@ Underscore template generate for FluentCV. var _ = require('underscore'); - module.exports = function( json, jst, format, cssInfo, opts ) { + module.exports = function( json, jst, format, cssInfo, opts, theme ) { // Tweak underscore's default template delimeters var delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template; diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 1fd47db..26859b4 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -139,11 +139,11 @@ Template-based resume generator base for FluentCV. @param cssInfo Needs to be refactored. @param opts Options and passthrough data. */ - single: function( json, jst, format, cssInfo, opts ) { + single: function( json, jst, format, cssInfo, opts, theme ) { this.opts.freezeBreaks && ( jst = freeze(jst) ); var eng = require( '../eng/' + ((opts.themeObj && opts.themeObj.engine) || opts.engine) + '-generator' ); - var result = eng( json, jst, format, cssInfo, opts ); + var result = eng( json, jst, format, cssInfo, opts, theme ); this.opts.freezeBreaks && ( result = unfreeze(result) ); return result; } @@ -193,12 +193,13 @@ Template-based resume generator base for FluentCV. */ function transform( rez, f, tplInfo, theme, outFolder ) { var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null }; - var mk = this.single( rez, tplInfo.data, this.format, cssInfo, this.opts ); + var mk = this.single( rez, tplInfo.data, this.format, cssInfo, this.opts, theme ); this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); var thisFilePath = PATH.join( outFolder, tplInfo.orgPath ); try { MKDIRP.sync( PATH.dirname( tplInfo.major ? f : thisFilePath) ); FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); + this.onAfterSave && (mk = this.onAfterSave( { outputFile: (tplInfo.major ? f : thisFilePath), mk: mk } )); } catch(ex) { console.log(ex); diff --git a/src/gen/word-generator.js b/src/gen/word-generator.js index db6cbd8..0ca3f01 100644 --- a/src/gen/word-generator.js +++ b/src/gen/word-generator.js @@ -3,11 +3,16 @@ MS Word resume generator for FluentCV. @license Copyright (c) 2015 by James M. Devlin. All rights reserved. */ -var TemplateGenerator = require('./template-generator'); -var WordGenerator = module.exports = TemplateGenerator.extend({ +(function() { - init: function(){ - this._super( 'doc', 'xml' ); - }, + var TemplateGenerator = require('./template-generator'); + var WordGenerator = module.exports = TemplateGenerator.extend({ -}); + init: function(){ + this._super( 'doc', 'xml' ); + } + + }); + + +}()); diff --git a/src/utils/html-to-wpml.js b/src/utils/html-to-wpml.js new file mode 100644 index 0000000..b0136ef --- /dev/null +++ b/src/utils/html-to-wpml.js @@ -0,0 +1,59 @@ + +(function(){ + + var _ = require('underscore'); + var HTML5Tokenizer = require('simple-html-tokenizer'); + + module.exports = function( html ) { + + var final = ''; + var is_bold = false, is_italic = false; + var depth = 0; + + var tokens = HTML5Tokenizer.tokenize( html ); + _.each( tokens, function( tok ) { + switch( tok.type ) { + case 'StartTag': + switch( tok.tagName ) { + case 'p': + final += ''; + break; + case 'strong': + is_bold = true; + break; + case 'em': + is_italic = true; + break; + case 'a': + is_link = true; + break; + } + break; + case 'EndTag': + switch( tok.tagName ) { + case 'p': + final += ''; + break; + case 'strong': + is_bold = false; + break; + case 'em': + is_italic = false; + break; + case 'a': + is_link = false; + break; + } + break; + case 'Chars': + var style = is_bold ? '' : ''; + style += is_italic ? '': ''; + final += '' + style + '' + tok.chars + ''; + break; + } + }); + return final; + + }; + +}()); diff --git a/tests/resumes/jrs/jane-doe.json b/tests/resumes/jrs/jane-doe.json new file mode 100644 index 0000000..3f98b1f --- /dev/null +++ b/tests/resumes/jrs/jane-doe.json @@ -0,0 +1,224 @@ +{ + "basics": { + "name": "Jane Doe", + "label": "Senior Developer / Code Ninja", + "summary": "**Full-stack software developer with 6+ years industry experience** specializing in scalable cloud architectures for this, that, and the other. A native of southern CA, Jane enjoys hiking, mystery novels, and the company of Rufus, her two-year-old beagle.", + "website": "http://jane-doe.me", + "phone": "1-650-999-7777", + "email": "jdoe@onecoolstartup.io", + "picture": "jane_doe.png", + "location": { + "address": "Jane Doe\n123 Somewhere Rd.\nMountain View, CA 94035", + "postalCode": "94035", + "city": "Mountain View", + "countryCode": "US", + "region": "CA" + }, + "profiles": [ + { + "network": "GitHub", + "username": "jane-doe-was-here", + "url": "https://github.com/jane-doe-was-here" + }, + { + "network": "Twitter", + "username": "jane-doe-was-here", + "url": "https://twitter.com/jane-doe-was-here" + } + ] + }, + "work": [ + { + "company": "One Cool Startup", + "website": "https://onecool.io/does-not-exist", + "position": "Head Code Ninja", + "startDate": "2013-09", + "summary": "Development team manager for OneCoolApp and OneCoolWebsite, a free social network tiddlywink generator and lifestyle portal with over 200,000 users.", + "highlights": [ + "Managed a 5-person development team", + "Accomplishment 2", + "Etc." + ] + }, + { + "company": "Veridian Dynamics", + "website": "https://en.wikipedia.org/wiki/Better_Off_Ted#Plot", + "position": "Principal Developer", + "startDate": "2011-07", + "endDate": "2013-08", + "summary": "Developer on numerous projects culminating in technical lead role for the [Jabberwocky project](http://betteroffted.wikia.com/wiki/Jabberwocky) and promotion to principal developer.", + "highlights": [ + "Managed a 5-person development team", + "Accomplishment 2", + "Etc." + ] + }, + { + "company": "Stark Industries", + "position": "IT Administrator", + "startDate": "2008-10", + "endDate": "2011-06", + "summary": "Junior programmer with heavy code responsibilities. Promoted to intermediate role after 6 months.", + "highlights": [ + "Promoted to intermediate developer after 6 months", + "Accomplishment 2", + "Etc." + ] + }, + { + "company": "Dunder Mifflin", + "position": "Intern", + "startDate": "2008-06", + "endDate": "2008-09", + "summary": "Performed IT administration and deployments for Dunder Mifflin.", + "highlights": [ + "Supervised roll-out of Dunder Mifflin Infinity website.", + "Performed mission-critical system backups and ", + "Etc." + ] + } + ], + "education": [ + { + "institution": "Cornell University", + "gpa": "3.5", + "courses": [ + "Course 1", + "Course 2", + "Course 2" + ], + "startDate": "2005-09", + "endDate": "2008-05" + }, + { + "institution": "Medfield College", + "gpa": "3.2", + "courses": [ + "Course 1", + "Course 2", + "Course 2" + ], + "startDate": "2003-09", + "endDate": "2005-06" + } + ], + "skills": [ + { + "name": "Programming", + "keywords": [ + "C++", + "Ruby", + "Xcode" + ] + }, + { + "name": "Project Management", + "keywords": [ + "Agile" + ] + } + ], + "volunteer": [ + { + "organization": "Technology for Tots", + "position": "Technical Consultant", + "startDate": "2003-11", + "endDate": "2005-06", + "website": "http://technology-for-tots.org", + "summary": "Summary of this volunteer stint.", + "highlights": [ + "Accomplishment 1", + "Accomplishment 2", + "etc" + ] + }, + { + "organization": "US Army Reserves", + "position": "NCO", + "startDate": "1999-11", + "endDate": "2003-06", + "website": "http://www.usar.army.mil/", + "summary": "Summary of this military stint.", + "highlights": [ + "Accomplishment 1", + "Accomplishment 2", + "etc" + ] + } + ], + "awards": [ + { + "title": "Honorable Mention", + "date": "2012", + "awarder": "Google" + }, + { + "title": "Summa cum laude", + "date": "2012", + "awarder": "Cornell University" + } + ], + "publications": [ + { + "name": "Building User Interfaces with Electron and Atom", + "publisher": "Code Project", + "releaseDate": "2011", + "website": "http://codeproject.com/build-ui-electron-atom.aspx" + }, + { + "name": "Jane Doe Unplugged", + "publisher": "self", + "releaseDate": "2011", + "website": "http://jane-doe.me" + }, + { + "name": "Teach Yourself GORFF in 21 Days", + "publisher": "Amazon", + "releaseDate": "2008", + "website": "http://url.to.publication.com/blah", + "summary": "A primer on the programming language of GORFF, whose for loops are coterminous with all of time and space." + } + ], + "interests": [ + { + "name": "reading", + "summary": "Jane is a fan of mystery novels and courtroom dramas including Agatha Christie and John Grisham.", + "keywords": [ + "mystery", + "Agatha Christie", + "John Grisham" + ] + }, + { + "name": "hiking", + "summary": "Jane enjoys hiking, light mountain climbing, and has four summits under her belt!" + }, + { + "name": "yoga" + } + ], + "references": [ + { + "name": "John Davidson", + "reference": "Jane is awesome! I'd hire her again in a heartbeat." + }, + { + "name": "Elias Fullstacker", + "reference": "I worked with Jane on Jabberwocky and can vouch for her awesome technical capabilities and attention to detail. Insta-hire." + }, + { + "name": "Dana Nevins", + "reference": "I've known Jane personally and professionally for almost ten years. She is one in a million." + } + ], + "languages": [ + { + "language": "English", + "level": "Native" + }, + { + "language": "Spanish", + "level": "Moderate" + } + ] +} diff --git a/tests/jrs-exemplar/richard-hendriks.json b/tests/resumes/jrs/richard-hendriks.json similarity index 100% rename from tests/jrs-exemplar/richard-hendriks.json rename to tests/resumes/jrs/richard-hendriks.json diff --git a/tests/test-converter.js b/tests/test-converter.js index ac778e2..134f086 100644 --- a/tests/test-converter.js +++ b/tests/test-converter.js @@ -17,8 +17,8 @@ describe('FRESH/JRS converter', function () { it('should round-trip from JRS to FRESH to JRS without modifying or losing data', function () { - var fileA = path.join( __dirname, 'jrs-exemplar/richard-hendriks.json' ); - var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' ); + var fileA = path.join( __dirname, 'resumes/jrs/richard-hendriks.json' ); + var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' ); _sheet = new FRESHResume().open( fileA ); _sheet.saveAs( fileB, 'JRS' ); diff --git a/tests/test-fresh-sheet.js b/tests/test-fresh-sheet.js index d7aa2f5..295f1ed 100644 --- a/tests/test-fresh-sheet.js +++ b/tests/test-fresh-sheet.js @@ -65,9 +65,3 @@ describe('jane-doe.json (FRESH)', function () { }); - -// describe('subtract', function () { -// it('should return -1 when passed the params (1, 2)', function () { -// expect(math.subtract(1, 2)).to.equal(-1); -// }); -// }); diff --git a/tests/test-jrs-sheet.js b/tests/test-jrs-sheet.js index 3afcba8..815b3aa 100644 --- a/tests/test-jrs-sheet.js +++ b/tests/test-jrs-sheet.js @@ -9,13 +9,14 @@ var chai = require('chai') chai.config.includeStack = false; -describe('fullstack.json (JRS)', function () { +describe('jane-doe.json (JRS)', function () { var _sheet; it('should open without throwing an exception', function () { function tryOpen() { - _sheet = new JRSResume().open( 'node_modules/resample/fullstack/in/resume.json' ); + _sheet = new JRSResume().open( + path.join( __dirname, 'resumes/jrs/jane-doe.json' ) ); } tryOpen.should.not.Throw(); }); @@ -32,36 +33,31 @@ describe('fullstack.json (JRS)', function () { ).to.equal( true ); }); - it('should have a work duration of 11 years', function() { - _sheet.computed.numYears.should.equal( 11 ); + it('should have a work duration of 7 years', function() { + _sheet.basics.computed.numYears.should.equal( 7 ); }); it('should save without throwing an exception', function(){ function trySave() { - _sheet.save( 'tests/sandbox/fullstack.json' ); + _sheet.save( 'tests/sandbox/jane-doe.json' ); } trySave.should.not.Throw(); }); it('should not be modified after saving', function() { - var savedSheet = new JRSResume().open( 'tests/sandbox/fullstack.json' ); + var savedSheet = new JRSResume().open( 'tests/sandbox/jane-doe.json' ); _sheet.stringify().should.equal( savedSheet.stringify() ) }); it('should validate against the JSON Resume schema', function() { - var schemaJson = require('../src/core/resume.json'); - var validate = validator( schemaJson, { verbose: true } ); - var result = validate( JSON.parse( _sheet.imp.raw ) ); + var result = _sheet.isValid(); + // var schemaJson = require('../src/core/resume.json'); + // var validate = validator( schemaJson, { verbose: true } ); + // var result = validate( JSON.parse( _sheet.imp.raw ) ); result || console.log("\n\nOops, resume didn't validate. " + - "Validation errors:\n\n", validate.errors, "\n\n"); + "Validation errors:\n\n", _sheet.basics.imp.validationErrors, "\n\n"); result.should.equal( true ); }); }); - -// describe('subtract', function () { -// it('should return -1 when passed the params (1, 2)', function () { -// expect(math.subtract(1, 2)).to.equal(-1); -// }); -// }); diff --git a/tests/test-themes.js b/tests/test-themes.js index d01b487..c224eda 100644 --- a/tests/test-themes.js +++ b/tests/test-themes.js @@ -26,85 +26,27 @@ describe('Testing themes', function () { useful: 'green', }); - it('HELLO-WORLD theme should generate without throwing an exception', function () { - function tryOpen() { - var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; - var dst = ['tests/sandbox/hello-world/resume.all']; - var opts = { - theme: 'hello-world', - format: 'FRESH', - prettify: true, - silent: false - }; - FCMD.verbs.build( src, dst, opts ); - } - tryOpen.should.not.Throw(); - }); + function genTheme( themeName ) { + it( themeName.toUpperCase() + ' theme should generate without throwing an exception', function () { + function tryOpen() { + var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; + var dst = ['tests/sandbox/hello-world/resume.all']; + var opts = { + theme: themeName, + format: 'FRESH', + prettify: true, + silent: false + }; + FCMD.verbs.build( src, dst, opts ); + } + tryOpen.should.not.Throw(); + }); + } - it('COMPACT theme should generate without throwing an exception', function () { - function tryOpen() { - var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; - var dst = ['tests/sandbox/compact/resume.all']; - var opts = { - theme: 'compact', - format: 'FRESH', - prettify: true, - silent: false - }; - FCMD.verbs.build( src, dst, opts ); - } - tryOpen.should.not.Throw(); - }); - - it('MODERN theme should generate without throwing an exception', function () { - function tryOpen() { - var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; - var dst = ['tests/sandbox/modern/resume.all']; - var opts = { - theme: 'modern', - format: 'FRESH', - prettify: true, - silent: false - }; - FCMD.verbs.build( src, dst, opts ); - } - tryOpen.should.not.Throw(); - }); - - it('MINIMIST theme should generate without throwing an exception', function () { - function tryOpen() { - var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; - var dst = ['tests/sandbox/minimist/resume.all']; - var opts = { - theme: 'minimist', - format: 'FRESH', - prettify: true, - silent: false - }; - FCMD.verbs.build( src, dst, opts ); - } - tryOpen.should.not.Throw(); - }); - - it('AWESOME theme should generate without throwing an exception', function () { - function tryOpen() { - var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; - var dst = ['tests/sandbox/awesome/resume.all']; - var opts = { - theme: 'awesome', - format: 'FRESH', - prettify: true, - silent: false - }; - FCMD.verbs.build( src, dst, opts ); - } - tryOpen.should.not.Throw(); - }); + genTheme('hello-world'); + genTheme('compact'); + genTheme('modern'); + genTheme('minimist'); + genTheme('awesome'); }); - -// describe('subtract', function () { -// it('should return -1 when passed the params (1, 2)', function () { -// expect(math.subtract(1, 2)).to.equal(-1); -// }); -// });