+
+ For more information, see the README."
+ afterGenerate:
+ msg:
+ - " (with %s)"
+ - "Skipping %s resume: %s"
+ - "Generating **%s** resume: **%s**"
+ beforeAnalyze:
+ msg: "Analyzing **%s** resume: **%s**"
+ beforeConvert:
+ msg: "Converting **%s** (**%s**) to **%s** (**%s**)"
+ afterValidate:
+ msg:
+ - "Validating **%s** against the **%s** schema: "
+ - "VALID!"
+ - "INVALID"
+ - "BROKEN"
+ - "MISSING"
+ - "ERROR"
+ beforePeek:
+ msg:
+ - Peeking at **%s** in **%s**
+ - Peeking at **%s**
+ afterPeek:
+ msg: "The specified key **%s** was not found in **%s**."
+ afterInlineConvert:
+ msg: Converting **%s** to **%s** format.
+errors:
+ themeNotFound:
+ msg: >
+ **Couldn't find the '%s' theme.** Please specify the name of a preinstalled
+ FRESH theme or the path to a locally installed FRESH or JSON Resume theme.
+ copyCSS:
+ msg: Couldn't copy CSS file to destination folder.
+ resumeNotFound:
+ msg: Please **feed me a resume** in FRESH or JSON Resume format.
+ missingCommand:
+ msg: Please **give me a command**
+ invalidCommand:
+ msg: Invalid command: '%s'
+ resumeNotFoundAlt:
+ msg: Please **feed me a resume** in either FRESH or JSON Resume format.
+ inputOutputParity:
+ msg: Please **specify an output file name** for every input file you wish to convert.
+ createNameMissing:
+ msg: Please **specify the filename** of the resume to create.
+ pdfGeneration:
+ msg: PDF generation failed. Make sure wkhtmltopdf is installed and accessible from your path.
+ invalid:
+ msg: Validation failed and the --assert option was specified.
+ invalidFormat:
+ msg: The **%s** theme doesn't support the **%s** format.
+ notOnPath:
+ msg: %s wasn't found on your system path or is inaccessible. PDF not generated.
+ readError:
+ msg: Reading **???** resume: **%s**
+ parseError:
+ msg:
+ - Invalid or corrupt JSON on line %s column %s.
+ - Invalid or corrupt JSON on line %s.
+ - Invalid or corrupt JSON.
+ invalidHelperUse:
+ msg: "**Warning**: Incorrect use of the **%s** theme helper."
+ fileSaveError:
+ msg: An error occurred while writing %s to disk: %s.
+ mixedMerge:
+ msg: "**Warning:** merging mixed resume types. Errors may occur."
+ invokeTemplate:
+ msg: "An error occurred during template invocation."
+ compileTemplate:
+ msg: "An error occurred during template compilation."
+ themeLoad:
+ msg: "Applying **%s** theme (? formats)"
+ invalidParamCount:
+ msg: "Invalid number of parameters. Expected: **%s**."
+ missingParam:
+ msg: The '**%s**' parameter was needed but not supplied.
+ createError:
+ msg: Failed to create **'%s'**.
+ exiting:
+ msg: Exiting with status code **%s**.
+ validateError:
+ msg: "An error occurred during validation:\n%s"
+ invalidOptionsFile:
+ msg:
+ - "The specified options file is invalid:\n"
+ - "\nMake sure the options file contains valid JSON."
+ optionsFileNotFound:
+ msg: "The specified options file is missing or inaccessible."
+ unknownSchema:
+ msg:
+ - "Unknown resume schema. Did you specify a valid FRESH or JRS resume?"
+ - |
+ At a minimum, a FRESH resume must include a "name" field and a "meta"
+ property.
+
+ "name": "John Doe",
+ "meta": {
+ "format": "FRESH@0.1.0"
+ }
+
+ JRS-format resumes must include a "basics" section with a "name":
+
+ "basics": {
+ "name": "John Doe"
+ }
+ themeHelperLoad:
+ msg: >-
+ An error occurred while attempting to load the '%s' theme helper. Is the
+ theme correctly installed?
+ dummy: dontcare
+ invalidSchemaVersion:
+ msg: "'%s' is not recognized as a valid schema version."
diff --git a/src/cli/out.js b/src/cli/out.js
new file mode 100644
index 00000000..df9194d2
--- /dev/null
+++ b/src/cli/out.js
@@ -0,0 +1,204 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+/**
+Output routines for HackMyResume.
+@license MIT. See LICENSE.md for details.
+@module cli/out
+*/
+
+
+
+const chalk = require('chalk');
+const HME = require('../core/event-codes');
+const _ = require('underscore');
+const M2C = require('../utils/md2chalk.js');
+const PATH = require('path');
+const FS = require('fs');
+const EXTEND = require('extend');
+const HANDLEBARS = require('handlebars');
+const YAML = require('yamljs');
+let printf = require('printf');
+const pad = require('string-padding');
+const dbgStyle = 'cyan';
+
+
+
+/** A stateful output module. All HMR console output handled here. */
+class OutputHandler {
+
+
+
+ constructor( opts ) {
+ this.init(opts);
+ }
+
+
+
+ init(opts) {
+ this.opts = EXTEND( true, this.opts || { }, opts );
+ this.msgs = YAML.load(PATH.join( __dirname, 'msg.yml' )).events;
+ }
+
+
+
+ log() {
+ printf = require('printf');
+ const finished = printf.apply( printf, arguments );
+ return this.opts.silent || console.log( finished ); // eslint-disable-line no-console
+ }
+
+
+
+ do( evt ) {
+
+ const that = this;
+ const L = function() { return that.log.apply( that, arguments ); };
+
+ switch (evt.sub) {
+
+ case HME.begin:
+ return this.opts.debug &&
+ L( M2C( this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase() );
+
+ //when HME.beforeCreate
+ //L( M2C( this.msgs.beforeCreate.msg, 'green' ), evt.fmt, evt.file )
+ //break;
+
+ case HME.afterCreate:
+ L( M2C( this.msgs.beforeCreate.msg, evt.isError ? 'red' : 'green' ), evt.fmt, evt.file );
+ break;
+
+ case HME.beforeTheme:
+ return this.opts.debug &&
+ L( M2C( this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase() );
+
+ case HME.afterParse:
+ return L( M2C( this.msgs.afterRead.msg, 'gray', 'white.dim'), evt.fmt.toUpperCase(), evt.file );
+
+ case HME.beforeMerge:
+ var msg = '';
+ evt.f.reverse().forEach(function( a, idx ) {
+ return msg += printf( (idx === 0 ? this.msgs.beforeMerge.msg[0] : this.msgs.beforeMerge.msg[1]), a.file );
+ }
+ , this);
+ return L( M2C(msg, (evt.mixed ? 'yellow' : 'gray'), 'white.dim') );
+
+ case HME.applyTheme:
+ this.theme = evt.theme;
+ var numFormats = Object.keys( evt.theme.formats ).length;
+ return L( M2C(this.msgs.applyTheme.msg,
+ evt.status === 'error' ? 'red' : 'gray',
+ evt.status === 'error' ? 'bold' : 'white.dim'),
+ evt.theme.name.toUpperCase(),
+ numFormats, numFormats === 1 ? '' : 's' );
+
+ case HME.end:
+ if (evt.cmd === 'build') {
+ const themeName = this.theme.name.toUpperCase();
+ if (this.opts.tips && (this.theme.message || this.theme.render)) {
+ if (this.theme.message) {
+ L( M2C( this.msgs.afterBuild.msg[0], 'cyan' ), themeName );
+ return L( M2C( this.theme.message, 'white' ));
+ } else if (this.theme.render) {
+ L( M2C( this.msgs.afterBuild.msg[0], 'cyan'), themeName);
+ return L( M2C( this.msgs.afterBuild.msg[1], 'white'));
+ }
+ }
+ }
+ break;
+
+ case HME.afterGenerate:
+ var suffix = '';
+ if (evt.fmt === 'pdf') {
+ if (this.opts.pdf) {
+ if (this.opts.pdf !== 'none') {
+ suffix = printf( M2C( this.msgs.afterGenerate.msg[0], evt.error ? 'red' : 'green' ), this.opts.pdf );
+ } else {
+ L( M2C( this.msgs.afterGenerate.msg[1], 'gray' ), evt.fmt.toUpperCase(), evt.file );
+ return;
+ }
+ }
+ }
+
+ return L( M2C( this.msgs.afterGenerate.msg[2] + suffix, evt.error ? 'red' : 'green' ),
+ pad( evt.fmt.toUpperCase(),4,null,pad.RIGHT ),
+ PATH.relative( process.cwd(), evt.file ) );
+
+ case HME.beforeAnalyze:
+ return L( M2C( this.msgs.beforeAnalyze.msg, 'green' ), evt.fmt, evt.file);
+
+ case HME.afterAnalyze:
+ var { info } = evt;
+ var rawTpl = FS.readFileSync( PATH.join( __dirname, 'analyze.hbs' ), 'utf8');
+ HANDLEBARS.registerHelper( require('../helpers/console-helpers') );
+ var template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false });
+ var tot = 0;
+ info.keywords.forEach(g => tot += g.count);
+ info.keywords.totalKeywords = tot;
+ var output = template( info );
+ return this.log( chalk.cyan(output) );
+
+ case HME.beforeConvert:
+ return L( M2C( this.msgs.beforeConvert.msg, evt.error ? 'red' : 'green' ),
+ evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt
+ );
+
+ case HME.afterInlineConvert:
+ return L( M2C( this.msgs.afterInlineConvert.msg, 'gray', 'white.dim' ),
+ evt.file, evt.fmt );
+
+ case HME.afterValidate:
+ var style = 'red';
+ var adj = '';
+ var msgs = this.msgs.afterValidate.msg;
+ switch (evt.status) {
+ case 'valid': style = 'green'; adj = msgs[1]; break;
+ case 'invalid': style = 'yellow'; adj = msgs[2]; break;
+ case 'broken': style = 'red'; adj = msgs[3]; break;
+ case 'missing': style = 'red'; adj = msgs[4]; break;
+ case 'unknown': style = 'red'; adj = msgs[5]; break;
+ }
+ evt.schema = evt.schema.replace('jars','JSON Resume').toUpperCase();
+ L(M2C( msgs[0], 'white' ) + chalk[style].bold(adj), evt.file, evt.schema);
+
+ if (evt.violations) {
+ _.each(evt.violations, function(err) {
+ L( chalk.yellow.bold('--> ') +
+ chalk.yellow(err.field.replace('data.','resume.').toUpperCase() +
+ ' ' + err.message));
+ }
+ , this);
+ }
+ return;
+
+ case HME.afterPeek:
+ var sty = evt.error ? 'red' : ( evt.target !== undefined ? 'green' : 'yellow' );
+
+ // "Peeking at 'someKey' in 'someFile'."
+ if (evt.requested) {
+ L(M2C(this.msgs.beforePeek.msg[0], sty), evt.requested, evt.file);
+ } else {
+ L(M2C(this.msgs.beforePeek.msg[1], sty), evt.file);
+ }
+
+ // If the key was present, print it
+ if ((evt.target !== undefined) && !evt.error) {
+ // eslint-disable-next-line no-console
+ return console.dir( evt.target, { depth: null, colors: true } );
+
+ // If the key was not present, but no error occurred, print it
+ } else if (!evt.error) {
+ return L(M2C( this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file);
+
+ } else if (evt.error) {
+ return L(chalk.red( evt.error.inner.inner ));
+ }
+ break;
+ }
+ }
+}
+
+module.exports = OutputHandler;
diff --git a/src/core/convert.js b/src/core/convert.js
deleted file mode 100644
index 29f1a47b..00000000
--- a/src/core/convert.js
+++ /dev/null
@@ -1,372 +0,0 @@
-/**
-FRESH to JSON Resume conversion routiens.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module convert.js
-*/
-
-(function(){
-
- /**
- Convert between FRESH and JRS resume/CV formats.
- @class FRESHConverter
- */
- var FRESHConverter = module.exports = {
-
-
- /**
- Convert from JSON Resume format to FRESH.
- @method toFresh
- @todo Refactor
- */
- toFRESH: function( src, foreign ) {
-
- foreign = (foreign === undefined || foreign === null) ? true : foreign;
-
- return {
-
- name: src.basics.name,
-
- info: {
- label: src.basics.label,
- class: src.basics.class, // <--> round-trip
- image: src.basics.picture,
- brief: src.basics.summary
- },
-
- contact: {
- email: src.basics.email,
- phone: src.basics.phone,
- website: src.basics.website,
- other: src.basics.other // <--> round-trip
- },
-
- meta: meta( true, src.meta ),
-
- location: {
- city: src.basics.location.city,
- region: src.basics.location.region,
- country: src.basics.location.countryCode,
- code: src.basics.location.postalCode,
- address: src.basics.location.address
- },
-
- employment: employment( src.work, true ),
- education: education( src.education, true),
- service: service( src.volunteer, true),
- skills: skillsToFRESH( src.skills ),
- writing: writing( src.publications, true),
- recognition: recognition( src.awards, true, foreign ),
- social: social( src.basics.profiles, true ),
- interests: src.interests,
- testimonials: references( src.references, true ),
- languages: src.languages,
- disposition: src.disposition // <--> round-trip
- };
- },
-
- /**
- Convert from FRESH format to JSON Resume.
- @param foreign True if non-JSON-Resume properties should be included in
- the result, false if those properties should be excluded.
- @todo Refactor
- */
- toJRS: function( src, foreign ) {
-
- foreign = (foreign === undefined || foreign === null) ? false : foreign;
-
- return {
-
- basics: {
- name: src.name,
- label: src.info.label,
- class: foreign ? src.info.class : undefined,
- summary: src.info.brief,
- website: src.contact.website,
- phone: src.contact.phone,
- email: src.contact.email,
- picture: src.info.image,
- location: {
- address: src.location.address,
- postalCode: src.location.code,
- city: src.location.city,
- countryCode: src.location.country,
- region: src.location.region
- },
- profiles: social( src.social, false )
- },
-
- 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: references( src.testimonials, false ),
- samples: foreign ? src.samples : undefined,
- disposition: foreign ? src.disposition : undefined,
- languages: src.languages
-
- };
-
- }
-
- };
-
- function meta( direction, obj ) {
- if( !obj ) return obj; // preserve null and undefined
- if( direction ) {
- obj = obj || { };
- obj.format = obj.format || "FRESH@0.1.0";
- obj.version = obj.version || "0.1.0";
- }
- return obj;
- }
-
- function employment( obj, direction ) {
- if( !obj ) return obj;
- 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( !obj ) return obj;
- 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( !obj ) return obj;
- 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( !obj ) return obj;
- 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( !obj ) return obj;
- 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( !obj ) return obj;
- 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( !obj ) return obj;
- 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 {
- sets: skills.map(function(set) {
- return {
- name: set.name,
- level: set.level,
- skills: set.keywords
- };
- })
- };
- }
-
- function skillsToJRS( skills ) {
- var ret = [];
- if( skills.sets && skills.sets.length ) {
- ret = skills.sets.map(function(set){
- return {
- name: set.name,
- level: set.level,
- keywords: set.skills
- };
- });
- }
- else if( skills.list ) {
- ret = skills.list.map(function(sk){
- return {
- name: sk.name,
- level: sk.level,
- keywords: sk.keywords
- };
- });
- }
- return ret;
- }
-
-
-
-}());
diff --git a/src/core/default-formats.js b/src/core/default-formats.js
index a9620bf9..4cbf0d6a 100644
--- a/src/core/default-formats.js
+++ b/src/core/default-formats.js
@@ -1,19 +1,18 @@
-(function(){
+/*
+Event code definitions.
+@module core/default-formats
+@license MIT. See LICENSE.md for details.
+*/
- var FLUENT = require('../hackmyapi');
-
- /**
- Supported resume formats.
- */
- module.exports = [
- { name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() },
- { name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() },
- { name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() },
- { name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() },
- { name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() },
- { name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() },
- { name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() },
- { name: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() }
- ];
-
-}());
+/** Supported resume formats. */
+module.exports = [
+ { name: 'html', ext: 'html', gen: new (require('../generators/html-generator'))() },
+ { name: 'txt', ext: 'txt', gen: new (require('../generators/text-generator'))() },
+ { name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../generators/word-generator'))() },
+ { name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new (require('../generators/html-pdf-cli-generator'))() },
+ { name: 'png', ext: 'png', fmt: 'html', is: false, gen: new (require('../generators/html-png-generator'))() },
+ { name: 'md', ext: 'md', fmt: 'txt', gen: new (require('../generators/markdown-generator'))() },
+ { name: 'json', ext: 'json', gen: new (require('../generators/json-generator'))() },
+ { name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../generators/json-yaml-generator'))() },
+ { name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../generators/latex-generator'))() }
+];
diff --git a/src/core/default-options.js b/src/core/default-options.js
index bc9d6ea4..2aade8f6 100644
--- a/src/core/default-options.js
+++ b/src/core/default-options.js
@@ -1,13 +1,15 @@
-(function(){
+/*
+Event code definitions.
+@module core/default-options
+@license MIT. See LICENSE.md for details.
+*/
- module.exports = {
- theme: 'modern',
- prettify: { // ← See https://github.com/beautify-web/js-beautify#options
- indent_size: 2,
- unformatted: ['em','strong'],
- max_char: 80, // ← See lib/html.js in above-linked repo
- //wrap_line_length: 120, ← Don't use this
- }
- };
-
-}());
+module.exports = {
+ theme: 'modern',
+ prettify: { // ← See https://github.com/beautify-web/js-beautify#options
+ indent_size: 2,
+ unformatted: ['em','strong'],
+ max_char: 80
+ } // ← See lib/html.js in above-linked repo
+};
+ // wrap_line_length: 120, ← Don't use this
diff --git a/src/core/empty-fresh.json b/src/core/empty-fresh.json
deleted file mode 100644
index 356ea504..00000000
--- a/src/core/empty-fresh.json
+++ /dev/null
@@ -1,184 +0,0 @@
-{
-
- "name": "",
-
- "meta": {
- "format": "FRESH@0.1.0",
- "version": "0.1.0"
- },
-
- "info": {
- "label": "",
- "characterClass": "",
- "brief": "",
- "image": ""
- },
-
- "contact": {
- "website": "",
- "phone": "",
- "email": "",
- "other": []
- },
-
- "location": {
- "address": "",
- "city": "",
- "region": "",
- "code": "",
- "country": ""
- },
-
- "social": [
- {
- "label": "",
- "network": "",
- "user": "",
- "url": ""
- }
- ],
-
- "employment": {
- "summary": "",
- "history": [
- {
- "employer": "",
- "url": "",
- "position": "",
- "summary": "",
- "start": "",
- "end": "",
- "keywords": [],
- "highlights": []
- }
- ]
- },
-
- "education": {
- "summary": "",
- "level": "",
- "degree": "",
- "history": [
- {
- "institution": "",
- "url": "",
- "start": "",
- "end": "",
- "grade": "",
- "summary": "",
- "curriculum": []
- }
- ]
- },
-
- "service": {
- "summary": "",
- "history": [
- {
- "flavor": "",
- "position": "",
- "organization": "",
- "url": "",
- "start": "",
- "end": "",
- "summary": "",
- "highlights": []
- }
- ]
- },
-
- "skills": {
-
- "sets": [
- {
- "name": "",
- "level": "",
- "skills": []
- }
- ],
-
- "list": [ ]
- },
-
- "samples": [
- {
- "title": "",
- "summary": "",
- "url": "",
- "date": ""
- }
- ],
-
- "writing": [
- {
- "title": "",
- "flavor": "",
- "date": "",
- "publisher": {
- "name": "",
- "url": ""
- },
- "url": ""
- }
- ],
-
- "reading": [
- {
- "title": "",
- "flavor": "",
- "url": "",
- "author": ""
- }
- ],
-
- "recognition": [
- {
- "flavor": "",
- "from": "",
- "title": "",
- "event": "",
- "date": "",
- "summary": ""
- }
- ],
-
- "references": [
- {
- "name": "",
- "flavor": "",
- "private": true,
- "contact": [
- {
- "label": "",
- "flavor": "",
- "value": ""
- }
- ]
- }
- ],
-
- "testimonials": [
- {
- "name": "",
- "flavor": "",
- "quote": ""
- }
- ],
-
- "languages": [
- {
- "language": "",
- "level": "",
- "years": 0
- }
- ],
-
- "interests": [
- {
- "name": "",
- "summary": "",
- "keywords": []
- }
- ]
-
-}
diff --git a/src/core/event-codes.js b/src/core/event-codes.js
new file mode 100644
index 00000000..1cd6a361
--- /dev/null
+++ b/src/core/event-codes.js
@@ -0,0 +1,39 @@
+/*
+Event code definitions.
+@module core/event-codes
+@license MIT. See LICENSE.md for details.
+*/
+
+
+module.exports = {
+ error: -1,
+ success: 0,
+ begin: 1,
+ end: 2,
+ beforeRead: 3,
+ afterRead: 4,
+ beforeCreate: 5,
+ afterCreate: 6,
+ beforeTheme: 7,
+ afterTheme: 8,
+ beforeMerge: 9,
+ afterMerge: 10,
+ beforeGenerate: 11,
+ afterGenerate: 12,
+ beforeAnalyze: 13,
+ afterAnalyze: 14,
+ beforeConvert: 15,
+ afterConvert: 16,
+ verifyOutputs: 17,
+ beforeParse: 18,
+ afterParse: 19,
+ beforePeek: 20,
+ afterPeek: 21,
+ beforeInlineConvert: 22,
+ afterInlineConvert: 23,
+ beforeValidate: 24,
+ afterValidate: 25,
+ beforeWrite: 26,
+ afterWrite: 27,
+ applyTheme: 28
+};
diff --git a/src/core/fluent-date.js b/src/core/fluent-date.js
index 7b7c5f6c..4ee22e29 100644
--- a/src/core/fluent-date.js
+++ b/src/core/fluent-date.js
@@ -1,10 +1,20 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS104: Avoid inline assignments
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
/**
The HackMyResume date representation.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module fluent-date.js
+@license MIT. See LICENSE.md for details.
+@module core/fluent-date
*/
-var moment = require('moment');
+
+
+const moment = require('moment');
+require('../utils/string');
/**
Create a FluentDate from a string or Moment date object. There are a few date
@@ -22,63 +32,64 @@ deprecation warnings, it's recommended to either a) explicitly specify the date
format or b) use an ISO format. For clarity, we handle these cases explicitly.
@class FluentDate
*/
-function FluentDate( dt ) {
- this.rep = this.fmt( dt );
+
+class FluentDate {
+
+ constructor(dt) {
+ this.rep = this.fmt(dt);
+ }
+
+ static isCurrent(dt) {
+ return !dt || (String.is(dt) && /^(present|now|current)$/.test(dt));
+ }
}
-FluentDate/*.prototype*/.fmt = function( dt ) {
- if( (typeof dt === 'string' || dt instanceof String) ) {
+const months = {};
+const abbr = {};
+moment.months().forEach((m,idx) => months[m.toLowerCase()] = idx+1);
+moment.monthsShort().forEach((m,idx) => abbr[m.toLowerCase()]=idx+1);
+abbr.sept = 9;
+module.exports = FluentDate;
+
+FluentDate.fmt = function( dt, throws ) {
+
+ throws = ((throws === undefined) || (throws === null)) || throws;
+
+ if ((typeof dt === 'string') || dt instanceof String) {
dt = dt.toLowerCase().trim();
- if( /^(present|now|current)$/.test(dt) ) { // "Present", "Now"
+ if (/^(present|now|current)$/.test(dt)) { // "Present", "Now"
return moment();
- }
- else if( /^\D+\s+\d{4}$/.test(dt) ) { // "Mar 2015"
- var parts = dt.split(' ');
- var month = (months[parts[0]] || abbr[parts[0]]);
- var temp = parts[1] + '-' + (month < 10 ? '0' + month : month.toString());
- return moment( temp, 'YYYY-MM' );
- }
- else if( /^\d{4}-\d{1,2}$/.test(dt) ) { // "2015-03", "1998-4"
- return moment( dt, 'YYYY-MM' );
- }
- else if( /^\s*\d{4}\s*$/.test(dt) ) { // "2015"
- return moment( dt, 'YYYY' );
- }
- else if( /^\s*$/.test(dt) ) { // "", " "
- var defTime = {
- isNull: true,
- isBefore: function( other ) {
- return( other && !other.isNull ) ? true : false;
- },
- isAfter: function( other ) {
- return( other && !other.isNull ) ? false : false;
- },
- unix: function() { return 0; },
- format: function() { return ''; },
- diff: function() { return 0; }
- };
- return defTime;
- }
- else {
- var mt = moment( dt );
- if(mt.isValid())
+ } else if (/^\D+\s+\d{4}$/.test(dt)) { // "Mar 2015"
+ let left;
+ const parts = dt.split(' ');
+ const month = (months[parts[0]] || abbr[parts[0]]);
+ const temp = parts[1] + '-' + ((left = month < 10) != null ? left : `0${{month : month.toString()}}`);
+ return moment(temp, 'YYYY-MM');
+ } else if (/^\d{4}-\d{1,2}$/.test(dt)) { // "2015-03", "1998-4"
+ return moment(dt, 'YYYY-MM');
+ } else if (/^\s*\d{4}\s*$/.test(dt)) { // "2015"
+ return moment(dt, 'YYYY');
+ } else if (/^\s*$/.test(dt)) { // "", " "
+ return moment();
+ } else {
+ const mt = moment(dt);
+ if (mt.isValid()) {
return mt;
- throw 'Invalid date format encountered.';
+ }
+ if (throws) {
+ throw 'Invalid date format encountered.';
+ }
+ return null;
}
- }
- else {
- if( !dt ) {
+ } else {
+ if (!dt) {
return moment();
- }
- else if( dt.isValid && dt.isValid() )
+ } else if (dt.isValid && dt.isValid()) {
return dt;
- throw 'Unknown date object encountered.';
+ }
+ if (throws) {
+ throw 'Unknown date object encountered.';
+ }
+ return null;
}
};
-
-var months = {}, abbr = {};
-moment.months().forEach(function(m,idx){months[m.toLowerCase()]=idx+1;});
-moment.monthsShort().forEach(function(m,idx){abbr[m.toLowerCase()]=idx+1;});
-abbr.sept = 9;
-
-module.exports = FluentDate;
diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js
index 4754194f..adf5e2e2 100644
--- a/src/core/fresh-resume.js
+++ b/src/core/fresh-resume.js
@@ -1,199 +1,258 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
/**
Definition of the FRESHResume class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module fresh-resume.js
+@license MIT. See LICENSE.md for details.
+@module core/fresh-resume
*/
-(function() {
- var FS = require('fs')
- , extend = require('../utils/extend')
- , validator = require('is-my-json-valid')
- , _ = require('underscore')
- , __ = require('lodash')
- , PATH = require('path')
- , moment = require('moment')
- , MD = require('marked')
- , CONVERTER = require('./convert')
- , JRSResume = require('./jrs-resume');
- /**
- A FRESH-style resume in JSON or YAML.
- @class FreshResume
- */
- function FreshResume() {
+const FS = require('fs');
+const extend = require('extend');
+let validator = require('is-my-json-valid');
+const _ = require('underscore');
+const __ = require('lodash');
+const XML = require('xml-escape');
+const MD = require('marked');
+const CONVERTER = require('fresh-jrs-converter');
+const JRSResume = require('./jrs-resume');
+
+
+
+/**
+A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
+object is an instantiation of that JSON decorated with utility methods.
+@constructor
+*/
+class FreshResume {// extends AbstractResume
+
+
+ /** Initialize the the FreshResume from JSON string data. */
+ parse( stringData, opts ) {
+ this.imp = this.imp != null ? this.imp : {raw: stringData};
+ return this.parseJSON(JSON.parse( stringData ), opts);
}
+
+
/**
- Open and parse the specified FRESH 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.
+ 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.
+ @param rep {Object} The raw JSON representation.
+ @param opts {Object} Resume loading and parsing options.
+ {
+ date: Perform safe date conversion.
+ sort: Sort resume items by date.
+ compute: Prepare computed resume totals.
+ }
*/
- FreshResume.prototype.open = function( file, title ) {
- this.imp = { fileName: file };
- this.imp.raw = FS.readFileSync( file, 'utf8' );
- return this.parse( this.imp.raw, title );
- };
+ parseJSON( rep, opts ) {
+
+ let scrubbed;
+ if (opts && opts.privatize) {
+ // Ignore any element with the 'ignore: true' or 'private: true' designator.
+ const scrubber = require('../utils/resume-scrubber');
+ var ret = scrubber.scrubResume(rep, opts);
+ scrubbed = ret.scrubbed;
+ }
+
+ // Now apply the resume representation onto this object
+ extend(true, this, opts && opts.privatize ? scrubbed : rep);
+
+ // If the resume has already been processed, then we are being called from
+ // the .dupe method, and there's no need to do any post processing
+ if (!(this.imp != null ? this.imp.processed : undefined)) {
+ // Set up metadata TODO: Clean up metadata on the object model.
+ opts = opts || { };
+ if ((opts.imp === undefined) || opts.imp) {
+ this.imp = this.imp || { };
+ this.imp.title = (opts.title || this.imp.title) || this.name;
+ if (!this.imp.raw) {
+ this.imp.raw = JSON.stringify(rep);
+ }
+ }
+ this.imp.processed = true;
+ // 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 = {
+ numYears: this.duration(),
+ keywords: this.keywords()
+ });
+ }
- /**
- Save the sheet to disk (for environments that have disk access).
- */
- FreshResume.prototype.save = function( filename ) {
- this.imp.fileName = filename || this.imp.fileName;
- FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
return this;
- };
+ }
+
+
+
+ /** Save the sheet to disk (for environments that have disk access). */
+ save( filename ) {
+ this.imp.file = filename || this.imp.file;
+ FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
+ return this;
+ }
+
+
/**
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
- FreshResume.prototype.saveAs = function( filename, format ) {
+ saveAs( filename, format ) {
- 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( filename, JRSResume.stringify( newRep ), 'utf8' );
+ // If format isn't specified, default to FRESH
+ const safeFormat = (format && format.trim()) || 'FRESH';
+
+ // Validate against the FRESH version regex
+ // freshVersionReg = require '../utils/fresh-version-regex'
+ // if (not freshVersionReg().test( safeFormat ))
+ // throw badVer: safeFormat
+
+ const parts = safeFormat.split('@');
+
+ if (parts[0] === 'FRESH') {
+ this.imp.file = filename || this.imp.file;
+ FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
+
+ } else if (parts[0] === 'JRS') {
+ const useEdgeSchema = parts.length > 1 ? parts[1] === '1' : false;
+ const newRep = CONVERTER.toJRS(this, {edge: useEdgeSchema});
+ FS.writeFileSync(filename, JRSResume.stringify( newRep ), 'utf8');
+ } else {
+ throw {badVer: safeFormat};
}
return this;
- };
+ }
+
- FreshResume.prototype.dupe = function() {
- var rnew = new FreshResume();
- rnew.parse( this.stringify(), { } );
- return rnew;
- };
/**
- Convert the supplied object to a JSON string, sanitizing meta-properties along
- the way.
+ Duplicate this FreshResume instance.
+ This method first extend()s this object onto an empty, creating a deep copy,
+ and then passes the result into a new FreshResume instance via .parseJSON.
+ We do it this way to create a true clone of the object without re-running any
+ of the associated processing.
*/
- FreshResume.stringify = function( obj ) {
- function replacer( 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( obj, replacer, 2 );
- };
+ dupe() {
+ const jso = extend(true, { }, this);
+ const rnew = new FreshResume();
+ rnew.parseJSON(jso, { });
+ return rnew;
+ }
+
+
/**
- Create a copy of this resume in which all fields have been interpreted as
- Markdown.
+ Convert this object to a JSON string, sanitizing meta-properties along the
+ way.
*/
- FreshResume.prototype.markdownify = function() {
+ stringify() { return FreshResume.stringify(this); }
- var that = this;
- var ret = this.dupe();
- function MDIN(txt){
- return MD(txt || '' ).replace(/^\s*|<\/p>\s*$/gi, '');
- }
- // TODO: refactor recursion
- function markdownifyStringsInObject( obj, inline ) {
+ /**
+ 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).
+ TODO: Move this out of FRESHResume.
+ */
+ transformStrings( filt, transformer ) {
+ const ret = this.dupe();
+ const trx = require('../utils/string-transformer');
+ return trx(ret, filt, transformer);
+ }
- if( !obj ) return;
- inline = inline === undefined || inline;
- if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
- obj.forEach(function(elem, idx, ar){
- if( typeof elem === 'string' || elem instanceof String )
- ar[idx] = inline ? MDIN(elem) : MD( elem );
- else
- markdownifyStringsInObject( elem );
- });
- }
- else if (typeof obj === 'object') {
- Object.keys( obj ).forEach(function(key) {
- var sub = obj[key];
- if( typeof sub === 'string' || sub instanceof String ) {
- if( _.contains(['skills','url','start','end','date'], key) )
- return;
- if( key === 'summary' )
- obj[key] = MD( obj[key] );
- else
- obj[key] = inline ? MDIN( obj[key] ) : MD( obj[key] );
- }
- else
- markdownifyStringsInObject( sub );
- });
+ /**
+ Create a copy of this resume in which all fields have been interpreted as
+ Markdown.
+ */
+ markdownify() {
+
+ const MDIN = txt => MD(txt || '' ).replace(/^\s*
|<\/p>\s*$/gi, '');
+
+ const trx = function( key, val ) {
+ if (key === 'summary') {
+ return MD(val);
}
+ return MDIN(val);
+ };
- }
+ return this.transformStrings(['skills','url','start','end','date'], trx);
+ }
- Object.keys( ret ).forEach(function(member){
- markdownifyStringsInObject( ret[ member ] );
- });
- return ret;
- };
/**
- Convert this object to a JSON string, sanitizing meta-properties along the
- way. Don't override .toString().
+ Create a copy of this resume in which all fields have been interpreted as
+ Markdown.
*/
- FreshResume.prototype.stringify = function() {
- return FreshResume.stringify( this );
- };
+ xmlify() {
+ const trx = (key, val) => XML(val);
+ return this.transformStrings([], trx);
+ }
- /**
- 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.
- */
- FreshResume.prototype.parse = function( stringData, opts ) {
- // Parse the incoming JSON representation
- var rep = JSON.parse( stringData );
- // Convert JSON Resume to FRESH if necessary
- if( rep.basics ) {
- rep = CONVERTER.toFRESH( rep );
- rep.imp = rep.imp || { };
- rep.imp.orgFormat = 'JRS';
- }
+ /** Return the resume format. */
+ format() { return 'FRESH'; }
- // Now apply the resume representation onto this object
- extend( true, this, rep );
- // Set up metadata
- opts = opts || { };
- if( opts.imp === undefined || opts.imp ) {
- this.imp = this.imp || { };
- this.imp.title = (opts.title || this.imp.title) || this.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 = {
- numYears: this.duration(),
- keywords: this.keywords()
- });
- return this;
- };
/**
- Return a unique list of all keywords across all skills.
+ Return internal metadata. Create if it doesn't exist.
*/
- FreshResume.prototype.keywords = function() {
- var flatSkills = [];
- this.skills && this.skills.length &&
- (flatSkills = this.skills.map(function(sk) { return sk.name; }));
+ i() { return this.imp = this.imp || { }; }
+
+
+
+ /**
+ Return a unique list of all skills declared in the resume.
+ */
+
+ // TODO: Several problems here:
+ // 1) Confusing name. Easily confused with the keyword-inspector module, which
+ // parses resume body text looking for these same keywords. This should probably
+ // be renamed.
+ //
+ // 2) Doesn't bother trying to integrate skills.list with skills.sets if they
+ // happen to declare different skills, and if skills.sets declares ONE skill and
+ // skills.list declared 50, only 1 skill will be registered.
+ //
+ // 3) In the future, skill.sets should only be able to use skills declared in
+ // skills.list. That is, skills.list is the official record of a candidate's
+ // declared skills. skills.sets is just a way of grouping those into skillsets
+ // for easier consumption.
+
+ keywords() {
+ let flatSkills = [];
+ if (this.skills) {
+ if (this.skills.sets) {
+ flatSkills = this.skills.sets.map(sk => sk.skills).reduce( (a,b) => a.concat(b));
+ } else if (this.skills.list) {
+ flatSkills = flatSkills.concat( this.skills.list.map(sk => sk.name) );
+ }
+ flatSkills = _.uniq(flatSkills);
+ }
return flatSkills;
- },
+ }
+
+
/**
- Reset the sheet to an empty state.
+ Reset the sheet to an empty state. TODO: refactor/review
*/
- FreshResume.prototype.clear = function( clearMeta ) {
+ clear( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
- clearMeta && (delete this.imp);
+ if (clearMeta) { delete this.imp; }
delete this.computed; // Don't use Object.keys() here
delete this.employment;
delete this.service;
@@ -203,208 +262,217 @@ Definition of the FRESHResume class.
delete this.writing;
delete this.interests;
delete this.skills;
- delete this.social;
- };
+ return 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;
+ count( 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.
- */
- FreshResume.default = function() {
- return new FreshResume().open(
- PATH.join( __dirname, 'empty-fresh.json'), 'Empty' );
- };
- /**
- Add work experience to the sheet.
- */
- FreshResume.prototype.add = function( moniker ) {
- var defSheet = FreshResume.default();
- var newObject = defSheet[moniker].history ?
- $.extend( true, {}, defSheet[ moniker ].history[0] ) :
- (moniker === 'skills' ?
- $.extend( true, {}, defSheet.skills.sets[0] ) :
- $.extend( true, {}, defSheet[ moniker ][0] ));
+
+ /** Add work experience to the sheet. */
+ add( moniker ) {
+ const defSheet = FreshResume.default();
+ const 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 ] || [];
- if( this[ moniker ].history )
- this[ moniker ].history.push( newObject );
- else if( moniker === 'skills' )
- this.skills.sets.push( newObject );
- else
- 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;
- };
+ }
+
+
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
- FreshResume.prototype.hasProfile = function( socialNetwork ) {
+ hasProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
- return this.social && _.some( this.social, function(p) {
- return p.network.trim().toLowerCase() === socialNetwork;
- });
- };
+ return this.social && _.some(this.social, p => p.network.trim().toLowerCase() === socialNetwork);
+ }
- /**
- Return the specified network profile.
- */
- FreshResume.prototype.getProfile = function( socialNetwork ) {
+
+
+ /** Return the specified network profile. */
+ getProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
- return this.social && _.find( this.social, function(sn) {
- return sn.network.trim().toLowerCase() === socialNetwork;
- });
- };
+ return this.social && _.find(this.social, sn => sn.network.trim().toLowerCase() === socialNetwork);
+ }
+
+
/**
Return an array of profiles for the specified network, for when the user
has multiple eg. GitHub accounts.
*/
- FreshResume.prototype.getProfiles = function( socialNetwork ) {
+ getProfiles( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
- return this.social && _.filter( this.social, function(sn){
- return sn.network.trim().toLowerCase() === socialNetwork;
- });
- };
+ return this.social && _.filter(this.social, sn => sn.network.trim().toLowerCase() === socialNetwork);
+ }
- /**
- Determine if the sheet includes a specific skill.
- */
- FreshResume.prototype.hasSkill = function( skill ) {
+
+
+ /** Determine if the sheet includes a specific skill. */
+ hasSkill( skill ) {
skill = skill.trim().toLowerCase();
- return this.skills && _.some( this.skills, function(sk) {
- return sk.keywords && _.some( sk.keywords, function(kw) {
- return kw.trim().toLowerCase() === skill;
- });
- });
- };
+ return this.skills && _.some(this.skills, sk =>
+ sk.keywords && _.some(sk.keywords, kw => kw.trim().toLowerCase() === skill)
+ );
+ }
- /**
- Validate the sheet against the FRESH Resume schema.
- */
- FreshResume.prototype.isValid = function( info ) {
- var schemaObj = require('fresca');
- var validator = require('is-my-json-valid');
- var validate = validator( schemaObj, { // Note [1]
+
+
+ /** Validate the sheet against the FRESH Resume schema. */
+ isValid() {
+ const schemaObj = require('fresh-resume-schema');
+ validator = require('is-my-json-valid');
+ const validate = validator( schemaObj, { // See Note [1].
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
- var ret = validate( this );
- if( !ret ) {
+ const ret = validate(this);
+ if (!ret) {
this.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
}
return ret;
- };
+ }
+
+
+
+ duration(unit) {
+ const inspector = require('../inspectors/duration-inspector');
+ return inspector.run(this, 'employment.history', 'start', 'end', unit);
+ }
+
+
- /**
- Calculate the total duration of the sheet. Assumes this.work has been sorted
- by start date descending, perhaps via a call to Sheet.sort().
- @returns The total duration of the sheet's work history, that is, the number
- of years between the start date of the earliest job on the resume and the
- *latest end date of all jobs in the work history*. This last condition is for
- sheets that have overlapping jobs.
- */
- FreshResume.prototype.duration = function() {
- var empHist = __.get(this, 'employment.history');
- if( empHist && empHist.length ) {
- 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 && w.safe.end ) ? w.safe.end.unix() : moment().unix();
- });
- return careerLast.safe.end.diff( careerStart, 'years' );
- }
- return 0;
- };
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
- FreshResume.prototype.sort = function( ) {
-
- __.get(this, 'employment.history') && this.employment.history.sort( byDateDesc );
- __.get(this, 'education.history') && this.education.history.sort( byDateDesc );
- __.get(this, 'service.history') && this.service.history.sort( byDateDesc );
-
- // this.awards && this.awards.sort( function(a, b) {
- // return( a.safeDate.isBefore(b.safeDate) ) ? 1
- // : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
- // });
- this.writing && this.writing.sort( function(a, b) {
- return( a.safe.date.isBefore(b.safe.date) ) ? 1
- : ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0;
+ sort() {
+
+ const byDateDesc = function(a,b) {
+ if (a.safe.start.isBefore(b.safe.start)) {
+ return 1;
+ } else { if (a.safe.start.isAfter(b.safe.start)) { return -1; } else { return 0; } }
+ };
+
+ const sortSection = function( key ) {
+ const ar = __.get(this, key);
+ if (ar && ar.length) {
+ const datedThings = ar.filter(o => o.start);
+ return datedThings.sort( byDateDesc );
+ }
+ };
+
+ sortSection('employment.history');
+ sortSection('education.history');
+ sortSection('service.history');
+ sortSection('projects');
+
+ return this.writing && this.writing.sort(function(a, b) {
+ if (a.safe.date.isBefore(b.safe.date)) {
+ return 1;
+ } else { return ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0; }
});
+ }
+}
- function byDateDesc(a,b) {
- return( a.safe.start.isBefore(b.safe.start) ) ? 1
- : ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0;
- }
+
+/**
+Get the default (starter) sheet.
+*/
+FreshResume.default = () => new FreshResume().parseJSON(require('fresh-resume-starter').fresh);
+
+
+
+/**
+Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
+along the way.
+*/
+FreshResume.stringify = function( obj ) {
+ const replacer = function( key,value ) { // Exclude these keys from stringification
+ const exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
+ 'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'];
+ if (_.some( exKeys, val => key.trim() === val)) {
+ return undefined; } else { return value; }
};
+ return JSON.stringify(obj, replacer, 2);
+};
+
- /**
- Convert human-friendly dates into formal Moment.js dates for all collections.
- We don't want to lose the raw textual date as entered by the user, so we store
- the Moment-ified date as a separate property with a prefix of .safe. For ex:
- job.startDate is the date as entered by the user. job.safeStartDate is the
- parsed Moment.js date that we actually use in processing.
- */
- function _parseDates() {
- var _fmt = require('./fluent-date').fmt;
- var that = this;
+/**
+Convert human-friendly dates into formal Moment.js dates for all collections.
+We don't want to lose the raw textual date as entered by the user, so we store
+the Moment-ified date as a separate property with a prefix of .safe. For ex:
+job.startDate is the date as entered by the user. job.safeStartDate is the
+parsed Moment.js date that we actually use in processing.
+*/
+var _parseDates = function() {
+
+ const _fmt = require('./fluent-date').fmt;
+ const that = this;
- // TODO: refactor recursion
- function replaceDatesInObject( obj ) {
+ // TODO: refactor recursion
+ var replaceDatesInObject = function( obj ) {
- if( !obj ) return;
- if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
- obj.forEach(function(elem){
- replaceDatesInObject( elem );
- });
+ if (!obj) { return; }
+ if (Object.prototype.toString.call( obj ) === '[object Array]') {
+ obj.forEach(elem => replaceDatesInObject( elem ));
+ return;
+ } else if (typeof obj === 'object') {
+ if (obj._isAMomentObject || obj.safe) {
+ return;
}
- else if (typeof obj === 'object') {
- if( obj._isAMomentObject || obj.safe )
- return;
- Object.keys( obj ).forEach(function(key) {
- replaceDatesInObject( obj[key] );
- });
- ['start','end','date'].forEach( function(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 ) {
- obj.safe.end = _fmt('current');
- }
+ Object.keys( obj ).forEach(key => replaceDatesInObject(obj[key]));
+ ['start','end','date'].forEach(function(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) {
+ obj.safe.end = _fmt('current');
+ return;
}
- });
- }
+ }
+ });
+ return;
}
+ };
+ Object.keys( this ).forEach(function(member) {
+ replaceDatesInObject(that[member]);
+ });
+};
- Object.keys( this ).forEach(function(member){
- replaceDatesInObject( that[ member ] );
- });
- }
- /**
- Export the Sheet function/ctor.
- */
- module.exports = FreshResume;
+/** Export the Sheet function/ctor. */
+module.exports = FreshResume;
+
-}());
// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
// in addition to YYYY-MM-DD. The original regex:
diff --git a/src/core/fresh-theme.js b/src/core/fresh-theme.js
new file mode 100644
index 00000000..8e4ca8a0
--- /dev/null
+++ b/src/core/fresh-theme.js
@@ -0,0 +1,253 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+/**
+Definition of the FRESHTheme class.
+@module core/fresh-theme
+@license MIT. See LICENSE.md for details.
+*/
+
+
+
+const FS = require('fs');
+const _ = require('underscore');
+const PATH = require('path');
+const parsePath = require('parse-filepath');
+const EXTEND = require('extend');
+const HMSTATUS = require('./status-codes');
+const loadSafeJson = require('../utils/safe-json-loader');
+const READFILES = require('recursive-readdir-sync');
+
+
+
+/* A representation of a FRESH theme asset.
+@class FRESHTheme */
+class FRESHTheme {
+
+ constructor() {
+ this.baseFolder = 'src';
+ }
+
+ /* Open and parse the specified theme. */
+ open( themeFolder ) {
+
+ this.folder = themeFolder;
+
+ // Set up a formats hash for the theme
+ let formatsHash = { };
+
+ // Load the theme
+ const themeFile = PATH.join(themeFolder, 'theme.json');
+ const themeInfo = loadSafeJson(themeFile);
+ if (themeInfo.ex) {
+ throw{
+ fluenterror:
+ themeInfo.ex.op === 'parse'
+ ? HMSTATUS.parseError
+ : HMSTATUS.readError,
+ inner: themeInfo.ex.inner
+ };
+ }
+
+ // Move properties from the theme JSON file to the theme object
+ EXTEND(true, this, themeInfo.json);
+
+ // Check for an "inherits" entry in the theme JSON.
+ if (this.inherits) {
+ const cached = { };
+ _.each(this.inherits, function(th, key) {
+ // First, see if this is one of the predefined FRESH themes. There are
+ // only a handful of these, but they may change over time, so we need to
+ // query the official source of truth: the fresh-themes repository, which
+ // mounts the themes conveniently by name to the module object, and which
+ // is embedded locally inside the HackMyResume installation.
+ // TODO: merge this code with
+ let themePath;
+ const themesObj = require('fresh-themes');
+ if (_.has(themesObj.themes, th)) {
+ themePath = PATH.join(
+ parsePath( require.resolve('fresh-themes') ).dirname,
+ '/themes/',
+ th
+ );
+ } else {
+ const d = parsePath( th ).dirname;
+ themePath = PATH.join(d, th);
+ }
+
+ cached[ th ] = cached[th] || new FRESHTheme().open( themePath );
+ return formatsHash[ key ] = cached[ th ].getFormat( key );
+ });
+ }
+
+ // Load theme files
+ formatsHash = _load.call(this, formatsHash);
+
+ // Cache
+ this.formats = formatsHash;
+
+ // Set the official theme name
+ this.name = parsePath( this.folder ).name;
+ return this;
+ }
+
+ /* Determine if the theme supports the specified output format. */
+ hasFormat( fmt ) { return _.has(this.formats, fmt); }
+
+ /* Determine if the theme supports the specified output format. */
+ getFormat( fmt ) { return this.formats[ fmt ]; }
+}
+
+
+
+/* Load and parse theme source files. */
+var _load = function(formatsHash) {
+
+ const that = this;
+ const tplFolder = PATH.join(this.folder, this.baseFolder);
+
+ // Iterate over all files in the theme folder, producing an array, fmts,
+ // containing info for each file. While we're doing that, also build up
+ // the formatsHash object.
+ const fmts = READFILES(tplFolder).map(function(absPath) {
+ return _loadOne.call(this, absPath, formatsHash, tplFolder);
+ }
+ , this);
+
+ // Now, get all the CSS files...
+ this.cssFiles = fmts.filter(fmt => fmt && (fmt.ext === 'css'));
+
+ // For each CSS file, get its corresponding HTML file. It's possible that
+ // a theme can have a CSS file but *no* HTML file, as when a theme author
+ // creates a pure CSS override of an existing theme.
+ this.cssFiles.forEach(function(cssf) {
+ const idx = _.findIndex(fmts, fmt => fmt && (fmt.pre === cssf.pre) && (fmt.ext === 'html'));
+ cssf.major = false;
+ if (idx > -1) {
+ fmts[ idx ].css = cssf.data;
+ return fmts[ idx ].cssPath = cssf.path;
+ } else {
+ if (that.inherits) {
+ // Found a CSS file without an HTML file in a theme that inherits
+ // from another theme. This is the override CSS file.
+ return that.overrides = { file: cssf.path, data: cssf.data };
+ }
+ }});
+
+ // Now, save all the javascript file paths to a theme property.
+ const jsFiles = fmts.filter(fmt => fmt && (fmt.ext === 'js'));
+ this.jsFiles = jsFiles.map(jsf => jsf['path']);
+
+ return formatsHash;
+};
+
+
+/* Load a single theme file. */
+var _loadOne = function( absPath, formatsHash, tplFolder ) {
+
+ const pathInfo = parsePath(absPath);
+ if (pathInfo.basename.toLowerCase() === 'theme.json') { return; }
+
+ const absPathSafe = absPath.trim().toLowerCase();
+ let outFmt = '';
+ let act = 'copy';
+ let isPrimary = false;
+
+ // If this is an "explicit" theme, all files of importance are specified in
+ // the "transform" section of the theme.json file.
+ if (this.explicit) {
+
+ outFmt = _.find(Object.keys( this.formats ), function( fmtKey ) {
+ const fmtVal = this.formats[ fmtKey ];
+ return _.some(fmtVal.transform, function(fpath) {
+ const absPathB = PATH.join( this.folder, fpath ).trim().toLowerCase();
+ return absPathB === absPathSafe;
+ }
+ , this);
+ }
+ , this);
+ if (outFmt) { act = 'transform'; }
+ }
+
+ if (!outFmt) {
+ // If this file lives in a specific format folder within the theme,
+ // such as "/latex" or "/html", then that format is the implicit output
+ // format for all files within the folder
+ const portion = pathInfo.dirname.replace(tplFolder,'');
+ if (portion && portion.trim()) {
+ if (portion[1] === '_') { return; }
+ const reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
+ const res = reg.exec( portion );
+ if (res) {
+ if (res[1] !== 'partials') {
+ outFmt = res[1];
+ if (!this.explicit) { act = 'transform'; }
+ } else {
+ this.partials = this.partials || [];
+ this.partials.push( { name: pathInfo.name, path: absPath } );
+ return null;
+ }
+ }
+ }
+ }
+
+ // Otherwise, the output format is inferred from the filename, as in
+ // compact-[outputformat].[extension], for ex, compact-pdf.html
+ if (!outFmt) {
+ const idx = pathInfo.name.lastIndexOf('-');
+ outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx+1);
+ if (!this.explicit) { act = 'transform'; }
+ const defFormats = require('./default-formats');
+ isPrimary = _.some(defFormats, form => (form.name === outFmt) && (pathInfo.extname !== '.css'));
+ }
+
+ // Make sure we have a valid formatsHash
+ formatsHash[ outFmt ] = formatsHash[outFmt] || {
+ outFormat: outFmt,
+ files: []
+ };
+
+ // Move symlink descriptions from theme.json to the format
+ if (__guard__(this.formats != null ? this.formats[outFmt ] : undefined, x => x.symLinks)) {
+ formatsHash[ outFmt ].symLinks = this.formats[ outFmt ].symLinks;
+ }
+
+ // Create the file representation object
+ const obj = {
+ action: act,
+ primary: isPrimary,
+ path: absPath,
+ orgPath: PATH.relative(tplFolder, absPath),
+ ext: pathInfo.extname.slice(1),
+ title: friendlyName(outFmt),
+ pre: outFmt,
+ // outFormat: outFmt || pathInfo.name,
+ data: FS.readFileSync(absPath, 'utf8'),
+ css: null
+ };
+
+ // Add this file to the list of files for this format type.
+ formatsHash[ outFmt ].files.push( obj );
+ return obj;
+};
+
+
+
+/* Return a more friendly name for certain formats. */
+var friendlyName = function( val ) {
+ val = (val && val.trim().toLowerCase()) || '';
+ const friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
+ return friendly[val] || val;
+};
+
+
+
+module.exports = FRESHTheme;
+
+function __guard__(value, transform) {
+ return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
+}
diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js
index c9311a07..a382da19 100644
--- a/src/core/jrs-resume.js
+++ b/src/core/jrs-resume.js
@@ -1,269 +1,348 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
/**
Definition of the JRSResume class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module jrs-resume.js
+@license MIT. See LICENSE.md for details.
+@module core/jrs-resume
*/
-(function() {
- var FS = require('fs')
- , extend = require('../utils/extend')
- , validator = require('is-my-json-valid')
- , _ = require('underscore')
- , PATH = require('path')
- , moment = require('moment');
- /**
- The JRSResume class represent a specific JSON character sheet. When Sheet.open
- is called, we merge the loaded JSON sheet properties onto the Sheet instance
- via extend(), so a full-grown sheet object will have all of the methods here,
- plus a complement of JSON properties from the backing JSON file. That allows
- us to treat Sheet objects interchangeably with the loaded JSON model.
- @class JRSResume
- */
- function JRSResume() {
+const FS = require('fs');
+const extend = require('extend');
+let validator = require('is-my-json-valid');
+const _ = require('underscore');
+const PATH = require('path');
+const CONVERTER = require('fresh-jrs-converter');
+
+/**
+A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
+is an instantiation of that JSON decorated with utility methods.
+@class JRSResume
+*/
+
+
+class JRSResume {
+
+ static initClass() {
+ /** Reset the sheet to an empty state. */
+ // clear = function( clearMeta ) {
+ // clearMeta = ((clearMeta === undefined) && true) || clearMeta;
+ // if (clearMeta) { delete this.imp; }
+ // delete this.basics.computed; // Don't use Object.keys() here
+ // delete this.work;
+ // delete this.volunteer;
+ // delete this.education;
+ // delete this.awards;
+ // delete this.publications;
+ // delete this.interests;
+ // delete this.skills;
+ // return delete this.basics.profiles;
+ // };
+ // extends AbstractResume
}
- /**
- 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.
- */
- JRSResume.prototype.open = function( file, 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.basics.imp.fileName = filename || this.basics.imp.fileName;
- FS.writeFileSync( this.basics.imp.fileName, this.stringify( this ), 'utf8' );
- return this;
- };
- /**
- Convert this object to a JSON string, sanitizing meta-properties along the
- way. Don't override .toString().
- */
- 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',
- 'isModified', 'htmlPreview', 'display_progress_bar'],
- function( val ) { return key.trim() === val; }
- ) ? undefined : value;
- }
- return JSON.stringify( obj, replacer, 2 );
- };
+ /** Initialize the the JSResume from string. */
+ parse( stringData, opts ) {
+ this.imp = this.imp != null ? this.imp : {raw: stringData};
+ return this.parseJSON(JSON.parse( stringData ), opts);
+ }
+
- JRSResume.prototype.stringify = function() {
- return JRSResume.stringify( this );
- };
/**
- 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 JRSResume object from JSON.
+ Open and parse the specified JRS 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.
+ @param rep {Object} The raw JSON representation.
+ @param opts {Object} Resume loading and parsing options.
+ {
+ date: Perform safe date conversion.
+ sort: Sort resume items by date.
+ compute: Prepare computed resume totals.
+ }
*/
- JRSResume.prototype.parse = function( stringData, opts ) {
+ parseJSON( rep, opts ) {
+ let scrubbed;
opts = opts || { };
- var rep = JSON.parse( stringData );
+ if (opts.privatize) {
+ const scrubber = require('../utils/resume-scrubber');
+ // Ignore any element with the 'ignore: true' or 'private: true' designator.
+ var ret = scrubber.scrubResume(rep, opts);
+ scrubbed = ret.scrubbed;
+ }
+
+ // Extend resume properties onto ourself.
+ extend(true, this, opts.privatize ? scrubbed : rep);
- extend( true, this, rep );
// Set up metadata
- 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;
+ if (!(this.imp != null ? this.imp.processed : undefined)) {
+ // Set up metadata TODO: Clean up metadata on the object model.
+ opts = opts || { };
+ if ((opts.imp === undefined) || opts.imp) {
+ this.imp = this.imp || { };
+ this.imp.title = (opts.title || this.imp.title) || this.basics.name;
+ if (!this.imp.raw) {
+ this.imp.raw = JSON.stringify(rep);
+ }
+ }
+ this.imp.processed = true;
}
// 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.basics.computed = {
- numYears: this.duration(),
- keywords: this.keywords()
- });
+ ((opts.date === undefined) || opts.date) && _parseDates.call( this );
+ ((opts.sort === undefined) || opts.sort) && this.sort();
+ if ((opts.compute === undefined) || opts.compute) {
+ this.basics.computed = {
+ numYears: this.duration(),
+ keywords: this.keywords()
+ };
+ }
return this;
- };
+ }
- /**
- Return a unique list of all keywords across all skills.
- */
- JRSResume.prototype.keywords = function() {
- var flatSkills = [];
- if( this.skills && this.skills.length ) {
- this.skills.forEach( function( s ) {
- flatSkills = _.union( flatSkills, s.keywords );
- });
+
+
+ /** Save the sheet to disk (for environments that have disk access). */
+ save( filename ) {
+ this.imp.file = filename || this.imp.file;
+ FS.writeFileSync(this.imp.file, this.stringify( this ), 'utf8');
+ return this;
+ }
+
+
+
+ /** Save the sheet to disk in a specific format, either FRESH or JRS. */
+ saveAs( filename, format ) {
+ if (format === 'JRS') {
+ this.imp.file = filename || this.imp.file;
+ FS.writeFileSync( this.imp.file, this.stringify(), 'utf8' );
+ } else {
+ const newRep = CONVERTER.toFRESH(this);
+ const stringRep = CONVERTER.toSTRING(newRep);
+ FS.writeFileSync(filename, stringRep, 'utf8');
+ }
+ return this;
+ }
+
+
+
+ /** Return the resume format. */
+ format() { return 'JRS'; }
+
+
+
+ stringify() { return JRSResume.stringify( this ); }
+
+
+
+ /** Return a unique list of all keywords across all skills. */
+ keywords() {
+ let flatSkills = [];
+ if (this.skills && this.skills.length) {
+ this.skills.forEach( s => flatSkills = _.union(flatSkills, s.keywords));
}
return flatSkills;
- };
+ }
- /**
- Reset the sheet to an empty state.
- */
- JRSResume.prototype.clear = function( clearMeta ) {
- clearMeta = ((clearMeta === undefined) && true) || clearMeta;
- clearMeta && (delete this.imp);
- delete this.basics.computed; // Don't use Object.keys() here
- delete this.work;
- delete this.volunteer;
- delete this.education;
- delete this.awards;
- delete this.publications;
- delete this.interests;
- delete this.skills;
- delete this.basics.profiles;
- };
- /**
- Get the default (empty) sheet.
- */
- JRSResume.default = function() {
- return new JRSResume().open( PATH.join( __dirname, 'empty-jrs.json'), 'Empty' );
- };
/**
- Add work experience to the sheet.
+ Return internal metadata. Create if it doesn't exist.
+ JSON Resume v0.0.0 doesn't allow additional properties at the root level,
+ so tuck this into the .basic sub-object.
*/
- JRSResume.prototype.add = function( moniker ) {
- var defSheet = JRSResume.default();
- var newObject = $.extend( true, {}, defSheet[ moniker ][0] );
+ i() {
+ return this.imp = this.imp != null ? this.imp : { };
+ }
+
+
+
+ /** Add work experience to the sheet. */
+ add( moniker ) {
+ const defSheet = JRSResume.default();
+ const newObject = extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject );
return newObject;
- };
+ }
- /**
- Determine if the sheet includes a specific social profile (eg, GitHub).
- */
- JRSResume.prototype.hasProfile = function( socialNetwork ) {
+
+
+ /** Determine if the sheet includes a specific social profile (eg, GitHub). */
+ hasProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
- return this.basics.profiles && _.some( this.basics.profiles, function(p) {
- return p.network.trim().toLowerCase() === socialNetwork;
- });
- };
+ return this.basics.profiles && _.some(this.basics.profiles, p => p.network.trim().toLowerCase() === socialNetwork);
+ }
- /**
- Determine if the sheet includes a specific skill.
- */
- JRSResume.prototype.hasSkill = function( skill ) {
+
+
+ /** Determine if the sheet includes a specific skill. */
+ hasSkill( skill ) {
skill = skill.trim().toLowerCase();
- return this.skills && _.some( this.skills, function(sk) {
- return sk.keywords && _.some( sk.keywords, function(kw) {
- return kw.trim().toLowerCase() === skill;
- });
- });
- };
+ return this.skills && _.some(this.skills, sk =>
+ sk.keywords && _.some(sk.keywords, kw => kw.trim().toLowerCase() === skill)
+ );
+ }
- /**
- Validate the sheet against the JSON Resume schema.
- */
- JRSResume.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓
- 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, { // Note [1]
+
+
+ /** Validate the sheet against the JSON Resume schema. */
+ isValid( ) { // TODO: ↓ fix this path ↓
+ const schema = FS.readFileSync(PATH.join( __dirname, 'resume.json' ), 'utf8');
+ const schemaObj = JSON.parse(schema);
+ validator = require('is-my-json-valid');
+ const 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;
+ const temp = this.imp;
+ delete this.imp;
+ const ret = validate(this);
+ this.imp = temp;
+ if (!ret) {
+ this.imp = this.imp || { };
+ this.imp.validationErrors = validate.errors;
}
return ret;
- };
+ }
+
+
+
+ duration(unit) {
+ const inspector = require('../inspectors/duration-inspector');
+ return inspector.run(this, 'work', 'startDate', 'endDate', unit);
+ }
+
- /**
- Calculate the total duration of the sheet. Assumes this.work has been sorted
- by start date descending, perhaps via a call to Sheet.sort().
- @returns The total duration of the sheet's work history, that is, the number
- of years between the start date of the earliest job on the resume and the
- *latest end date of all jobs in the work history*. This last condition is for
- sheets that have overlapping jobs.
- */
- JRSResume.prototype.duration = function() {
- if( this.work && this.work.length ) {
- var careerStart = this.work[ this.work.length - 1].safeStartDate;
- if ((typeof careerStart === 'string' || careerStart instanceof String) &&
- !careerStart.trim())
- return 0;
- var careerLast = _.max( this.work, function( w ) {
- return w.safeEndDate.unix();
- }).safeEndDate;
- return careerLast.diff( careerStart, 'years' );
- }
- return 0;
- };
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
- JRSResume.prototype.sort = function( ) {
+ sort( ) {
+
+ const byDateDesc = function(a,b) {
+ if (a.safeStartDate.isBefore(b.safeStartDate)) {
+ return 1;
+ } else { return ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0; }
+ };
- this.work && this.work.sort( byDateDesc );
- this.education && this.education.sort( byDateDesc );
- this.volunteer && this.volunteer.sort( byDateDesc );
+ this.work && this.work.sort(byDateDesc);
+ this.education && this.education.sort(byDateDesc);
+ this.volunteer && this.volunteer.sort(byDateDesc);
- this.awards && this.awards.sort( function(a, b) {
- return( a.safeDate.isBefore(b.safeDate) ) ? 1
- : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
+ this.awards && this.awards.sort(function(a, b) {
+ if (a.safeDate.isBefore(b.safeDate)) {
+ return 1;
+ } else { return (a.safeDate.isAfter(b.safeDate) && -1 ) || 0; }
});
- this.publications && this.publications.sort( function(a, b) {
- return( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) ? 1
- : ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0;
+
+ return this.publications && this.publications.sort(function(a, b) {
+ if ( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) {
+ return 1;
+ } else { return ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0; }
});
+ }
+
+
+
+ dupe() {
+ const rnew = new JRSResume();
+ rnew.parse(this.stringify(), { });
+ return rnew;
+ }
- function byDateDesc(a,b) {
- return( a.safeStartDate.isBefore(b.safeStartDate) ) ? 1
- : ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0;
- }
- };
/**
- Convert human-friendly dates into formal Moment.js dates for all collections.
- We don't want to lose the raw textual date as entered by the user, so we store
- the Moment-ified date as a separate property with a prefix of .safe. For ex:
- job.startDate is the date as entered by the user. job.safeStartDate is the
- parsed Moment.js date that we actually use in processing.
+ Create a copy of this resume in which all fields have been interpreted as
+ Markdown.
*/
- function _parseDates() {
+ harden() {
- var _fmt = require('./fluent-date').fmt;
+ const ret = this.dupe();
- this.work && this.work.forEach( function(job) {
- job.safeStartDate = _fmt( job.startDate );
- job.safeEndDate = _fmt( job.endDate );
- });
- this.education && this.education.forEach( function(edu) {
- edu.safeStartDate = _fmt( edu.startDate );
- edu.safeEndDate = _fmt( edu.endDate );
- });
- this.volunteer && this.volunteer.forEach( function(vol) {
- vol.safeStartDate = _fmt( vol.startDate );
- vol.safeEndDate = _fmt( vol.endDate );
- });
- this.awards && this.awards.forEach( function(awd) {
- awd.safeDate = _fmt( awd.date );
- });
- this.publications && this.publications.forEach( function(pub) {
- pub.safeReleaseDate = _fmt( pub.releaseDate );
- });
+ const HD = txt => `@@@@~${txt}~@@@@`;
+
+ // const HDIN = txt =>
+ // //return MD(txt || '' ).replace(/^\s*
|<\/p>\s*$/gi, '');
+ // HD(txt)
+ // ;
+
+ const transformer = require('../utils/string-transformer');
+ return transformer(ret,
+ [ 'skills','url','website','startDate','endDate', 'releaseDate', 'date',
+ 'phone','email','address','postalCode','city','country','region',
+ 'safeStartDate','safeEndDate' ],
+ (key, val) => HD(val));
}
+}
+
+JRSResume.initClass();
+
+
+
+
+/** Get the default (empty) sheet. */
+JRSResume.default = () => new JRSResume().parseJSON(require('fresh-resume-starter').jrs);
- /**
- Export the JRSResume function/ctor.
- */
- module.exports = JRSResume;
-}());
+
+/**
+Convert this object to a JSON string, sanitizing meta-properties along the
+way. Don't override .toString().
+*/
+JRSResume.stringify = function( obj ) {
+ const replacer = function( key,value ) { // Exclude these keys from stringification
+ const temp = _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
+ 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
+ 'isModified', 'htmlPreview', 'display_progress_bar'],
+ val => key.trim() === val);
+ if (temp) { return undefined; } else { return value; }
+ };
+ return JSON.stringify(obj, replacer, 2);
+};
+
+
+
+/**
+Convert human-friendly dates into formal Moment.js dates for all collections.
+We don't want to lose the raw textual date as entered by the user, so we store
+the Moment-ified date as a separate property with a prefix of .safe. For ex:
+job.startDate is the date as entered by the user. job.safeStartDate is the
+parsed Moment.js date that we actually use in processing.
+*/
+var _parseDates = function() {
+
+ const _fmt = require('./fluent-date').fmt;
+
+ this.work && this.work.forEach(function(job) {
+ job.safeStartDate = _fmt( job.startDate );
+ return job.safeEndDate = _fmt( job.endDate );
+ });
+ this.education && this.education.forEach(function(edu) {
+ edu.safeStartDate = _fmt( edu.startDate );
+ return edu.safeEndDate = _fmt( edu.endDate );
+ });
+ this.volunteer && this.volunteer.forEach(function(vol) {
+ vol.safeStartDate = _fmt( vol.startDate );
+ return vol.safeEndDate = _fmt( vol.endDate );
+ });
+ this.awards && this.awards.forEach(awd => awd.safeDate = _fmt( awd.date ));
+ return this.publications && this.publications.forEach(pub => pub.safeReleaseDate = _fmt( pub.releaseDate ));
+};
+
+
+
+/**
+Export the JRSResume class.
+*/
+module.exports = JRSResume;
diff --git a/src/core/jrs-theme.js b/src/core/jrs-theme.js
new file mode 100644
index 00000000..4e84376d
--- /dev/null
+++ b/src/core/jrs-theme.js
@@ -0,0 +1,96 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+/**
+Definition of the JRSTheme class.
+@module core/jrs-theme
+@license MIT. See LICENSE.MD for details.
+*/
+
+
+
+const _ = require('underscore');
+const PATH = require('path');
+const pathExists = require('path-exists').sync;
+const errors = require('./status-codes');
+
+
+
+/**
+The JRSTheme class is a representation of a JSON Resume theme asset.
+@class JRSTheme
+*/
+class JRSTheme {
+
+
+
+ /**
+ Open and parse the specified JRS theme.
+ @method open
+ */
+ open( thFolder ) {
+
+ this.folder = thFolder;
+ //const pathInfo = parsePath(thFolder);
+
+ // Open and parse the theme's package.json file
+ const pkgJsonPath = PATH.join(thFolder, 'package.json');
+ if (pathExists(pkgJsonPath)) {
+ const thApi = require(thFolder); // Requiring the folder yields whatever the package.json's "main" is set to
+ const thPkg = require(pkgJsonPath); // Get the package.json as JSON
+ this.name = thPkg.name;
+ this.render = (thApi && thApi.render) || undefined;
+ this.engine = 'jrs';
+
+ // Create theme formats (HTML and PDF). Just add the bare minimum mix of
+ // properties necessary to allow JSON Resume themes to share a rendering
+ // path with FRESH themes.
+ this.formats = {
+ html: {
+ outFormat: 'html',
+ files: [{
+ action: 'transform',
+ render: this.render,
+ primary: true,
+ ext: 'html',
+ css: null
+ }]
+ },
+ pdf: {
+ outFormat: 'pdf',
+ files: [{
+ action: 'transform',
+ render: this.render,
+ primary: true,
+ ext: 'pdf',
+ css: null
+ }]
+ }
+ };
+ } else {
+ throw {fluenterror: errors.missingPackageJSON};
+ }
+ return this;
+ }
+
+
+
+ /**
+ Determine if the theme supports the output format.
+ @method hasFormat
+ */
+ hasFormat( fmt ) { return _.has(this.formats, fmt); }
+
+
+
+ /**
+ Return the requested output format.
+ @method getFormat
+ */
+ getFormat( fmt ) { return this.formats[ fmt ]; }
+}
+
+
+module.exports = JRSTheme;
diff --git a/src/core/load-source-resumes.js b/src/core/load-source-resumes.js
deleted file mode 100644
index b26ae70d..00000000
--- 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 00000000..f91d2d99
--- /dev/null
+++ b/src/core/resume-factory.js
@@ -0,0 +1,127 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+/**
+Definition of the ResumeFactory class.
+@license MIT. See LICENSE.md for details.
+@module core/resume-factory
+*/
+
+
+
+const FS = require('fs');
+const HMS = require('./status-codes');
+const HME = require('./event-codes');
+const ResumeConverter = require('fresh-jrs-converter');
+const resumeDetect = require('../utils/resume-detector');
+require('string.prototype.startswith');
+
+
+
+/**
+A simple factory class for FRESH and JSON Resumes.
+@class ResumeFactory
+*/
+
+module.exports = {
+
+
+
+ /**
+ Load one or more resumes from disk.
+
+ @param {Object} opts An options object with settings for the factory as well
+ as passthrough settings for FRESHResume or JRSResume. Structure:
+
+ {
+ format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null)
+ objectify: true, // FRESH/JRSResume or raw JSON?
+ inner: { // Passthru options for FRESH/JRSResume
+ sort: false
+ }
+ }
+
+ */
+ load( sources, opts, emitter ) {
+ return sources.map( function(src) {
+ return this.loadOne( src, opts, emitter );
+ }
+ , this);
+ },
+
+
+ /** Load a single resume from disk. */
+ loadOne( src, opts, emitter ) {
+
+ let toFormat = opts.format; // Can be null
+
+ // Get the destination format. Can be 'fresh', 'jrs', or null/undefined.
+ toFormat && (toFormat = toFormat.toLowerCase().trim());
+
+ // Load and parse the resume JSON
+ const info = _parse(src, opts, emitter);
+ if (info.fluenterror) { return info; }
+
+ // Determine the resume format: FRESH or JRS
+ let { json } = info;
+ const orgFormat = resumeDetect(json);
+ if (orgFormat === 'unk') {
+ info.fluenterror = HMS.unknownSchema;
+ return info;
+ }
+
+ // 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.
+ let rez = null;
+ if (opts.objectify) {
+ const reqLib = `../core/${toFormat || orgFormat}-resume`;
+ const ResumeClass = require(reqLib);
+ rez = new ResumeClass().parseJSON( json, opts.inner );
+ rez.i().file = src;
+ }
+
+ return {
+ file: src,
+ json: info.json,
+ rez
+ };
+ }
+};
+
+
+var _parse = function( fileName, opts, eve ) {
+
+ let rawData = null;
+ try {
+
+ // Read the file
+ eve && eve.stat( HME.beforeRead, { file: fileName });
+ rawData = FS.readFileSync( fileName, 'utf8' );
+ eve && eve.stat( HME.afterRead, { file: fileName, data: rawData });
+
+ // Parse the file
+ eve && eve.stat(HME.beforeParse, { data: rawData });
+ const ret = { json: JSON.parse( rawData ) };
+ const orgFormat =
+ ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@')
+ ? 'fresh' : 'jrs';
+
+ eve && eve.stat(HME.afterParse, { file: fileName, data: ret.json, fmt: orgFormat });
+ return ret;
+ } catch (err) {
+ // Can be ENOENT, EACCES, SyntaxError, etc.
+ return {
+ fluenterror: rawData ? HMS.parseError : HMS.readError,
+ inner: err,
+ raw: rawData,
+ file: fileName
+ };
+ }
+};
diff --git a/src/core/status-codes.js b/src/core/status-codes.js
new file mode 100644
index 00000000..e4649243
--- /dev/null
+++ b/src/core/status-codes.js
@@ -0,0 +1,41 @@
+/**
+Status codes for HackMyResume.
+@module core/status-codes
+@license MIT. See LICENSE.MD for details.
+*/
+
+
+module.exports = {
+ success: 0,
+ themeNotFound: 1,
+ copyCss: 2,
+ resumeNotFound: 3,
+ missingCommand: 4,
+ invalidCommand: 5,
+ resumeNotFoundAlt: 6,
+ inputOutputParity: 7,
+ createNameMissing: 8,
+ pdfGeneration: 9,
+ missingPackageJSON: 10,
+ invalid: 11,
+ invalidFormat: 12,
+ notOnPath: 13,
+ readError: 14,
+ parseError: 15,
+ fileSaveError: 16,
+ generateError: 17,
+ invalidHelperUse: 18,
+ mixedMerge: 19,
+ invokeTemplate: 20,
+ compileTemplate: 21,
+ themeLoad: 22,
+ invalidParamCount: 23,
+ missingParam: 24,
+ createError: 25,
+ validateError: 26,
+ invalidOptionsFile: 27,
+ optionsFileNotFound: 28,
+ unknownSchema: 29,
+ themeHelperLoad: 30,
+ invalidSchemaVersion: 31
+};
diff --git a/src/core/theme.js b/src/core/theme.js
deleted file mode 100644
index 3b5f729b..00000000
--- a/src/core/theme.js
+++ /dev/null
@@ -1,275 +0,0 @@
-/**
-Definition of the Theme class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module theme.js
-*/
-
-(function() {
-
- var FS = require('fs')
- , extend = require('../utils/extend')
- , validator = require('is-my-json-valid')
- , _ = require('underscore')
- , PATH = require('path')
- , parsePath = require('parse-filepath')
- , EXTEND = require('../utils/extend')
- , moment = require('moment')
- , RECURSIVE_READ_DIR = require('recursive-readdir-sync');
-
- /**
- The Theme class is a representation of a HackMyResume theme asset.
- @class Theme
- */
- function Theme() {
-
- }
-
- /**
- Open and parse the specified theme.
- */
- Theme.prototype.open = function( themeFolder ) {
-
- // Open the [theme-name].json file; should have the same name as folder
- this.folder = themeFolder;
- var pathInfo = parsePath( themeFolder );
- var themeFile = PATH.join( themeFolder, pathInfo.basename + '.json' );
- var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) );
- var that = this;
-
- // 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 ) {
- formatsHash = loadExplicit.call( this );
- this.explicit = true;
- }
- else {
- formatsHash = loadImplicit.call( this );
- }
-
- // Add freebie formats every theme gets
- formatsHash.json = { title: 'json', outFormat: 'json', pre: 'json', ext: 'json', path: null, data: null };
- formatsHash.yml = { title: 'yaml', outFormat: 'yml', pre: 'yml', ext: 'yml', path: null, data: null };
-
- // Cache
- this.formats = formatsHash;
-
- // Set the official theme name
- this.name = parsePath( this.folder ).name;
-
- return this;
- };
-
- /**
- Determine if the theme supports the specified output format.
- */
- Theme.prototype.hasFormat = function( fmt ) {
- return _.has( this.formats, fmt );
- };
-
- /**
- Determine if the theme supports the specified output format.
- */
- Theme.prototype.getFormat = function( fmt ) {
- return this.formats[ fmt ];
- };
-
- function loadImplicit() {
-
- // Set up a hash of formats supported by this theme.
- var formatsHash = { };
- var that = this;
- var major = false;
-
- // Establish the base theme folder
- var tplFolder = PATH.join( this.folder, 'src' );
-
- // Iterate over all files in the theme folder, producing an array, fmts,
- // containing info for each file. While we're doing that, also build up
- // the formatsHash object.
- var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) {
-
- // If this file lives in a specific format folder within the theme,
- // such as "/latex" or "/html", then that format is the output format
- // for all files within the folder.
- var pathInfo = parsePath(absPath);
- var outFmt = '', isMajor = false;
- var portion = pathInfo.dirname.replace(tplFolder,'');
- if( portion && portion.trim() ) {
- if( portion[1] === '_' ) return;
- var reg = /^(?:\/|\\)(html|latex|doc|pdf|partials)(?:\/|\\)?/ig;
- var res = reg.exec( portion );
- 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
- // compact-[outputformat].[extension], for ex, compact-pdf.html.
- if( !outFmt ) {
- var idx = pathInfo.name.lastIndexOf('-');
- outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 );
- isMajor = true;
- }
-
- // We should have a valid output format now.
- formatsHash[ outFmt ] = formatsHash[outFmt] || {
- outFormat: outFmt,
- files: []
- };
-
- // Create the file representation object.
- var obj = {
- action: 'transform',
- path: absPath,
- major: isMajor,
- orgPath: PATH.relative(tplFolder, absPath),
- ext: pathInfo.extname.slice(1),
- title: friendlyName( outFmt ),
- pre: outFmt,
- // outFormat: outFmt || pathInfo.name,
- data: FS.readFileSync( absPath, 'utf8' ),
- css: null
- };
-
- // Add this file to the list of files for this format type.
- formatsHash[ outFmt ].files.push( obj );
- return obj;
- });
-
- // Now, get all the CSS files...
- (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 ) {
- return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
- });
- cssf.action = null;
- fmts[ idx ].css = cssf.data;
- fmts[ idx ].cssPath = cssf.path;
- });
-
- // Remove CSS files from the formats array
- fmts = fmts.filter( function( fmt) {
- return fmt && (fmt.ext !== 'css');
- });
-
- return formatsHash;
- }
-
- function loadExplicit() {
-
- var that = this;
- // Set up a hash of formats supported by this theme.
- var formatsHash = { };
-
- // Establish the base theme folder
- var tplFolder = PATH.join( this.folder, 'src' );
-
- var act = null;
-
- // Iterate over all files in the theme folder, producing an array, fmts,
- // containing info for each file. While we're doing that, also build up
- // the formatsHash object.
- var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) {
-
- act = null;
- // If this file is mentioned in the theme's JSON file under "transforms"
- var pathInfo = parsePath(absPath);
- var absPathSafe = absPath.trim().toLowerCase();
- var outFmt = _.find( Object.keys( that.formats ), function( fmtKey ) {
- var fmtVal = that.formats[ fmtKey ];
- return _.some( fmtVal.transform, function( fpath ) {
- var absPathB = PATH.join( that.folder, fpath ).trim().toLowerCase();
- return absPathB === absPathSafe;
- });
- });
- if( outFmt ) {
- act = 'transform';
- }
-
- // If this file lives in a specific format folder within the theme,
- // such as "/latex" or "/html", then that format is the output format
- // for all files within the folder.
- if( !outFmt ) {
- var portion = pathInfo.dirname.replace(tplFolder,'');
- if( portion && portion.trim() ) {
- var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
- var res = reg.exec( portion );
- res && (outFmt = res[1]);
- }
- }
-
- // Otherwise, the output format is inferred from the filename, as in
- // compact-[outputformat].[extension], for ex, compact-pdf.html.
- if( !outFmt ) {
- var idx = pathInfo.name.lastIndexOf('-');
- outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 );
- }
-
- // We should have a valid output format now.
- formatsHash[ outFmt ] =
- formatsHash[ outFmt ] || {
- outFormat: outFmt,
- files: [],
- symLinks: that.formats[ outFmt ].symLinks
- };
-
- // Create the file representation object.
- var obj = {
- action: act,
- orgPath: PATH.relative(that.folder, absPath),
- path: absPath,
- ext: pathInfo.extname.slice(1),
- title: friendlyName( outFmt ),
- pre: outFmt,
- // outFormat: outFmt || pathInfo.name,
- data: FS.readFileSync( absPath, 'utf8' ),
- css: null
- };
-
- // Add this file to the list of files for this format type.
- formatsHash[ outFmt ].files.push( obj );
- return obj;
- });
-
- // Now, get all the CSS files...
- (this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; }))
- .forEach(function( cssf ) {
- // For each CSS file, get its corresponding HTML file
- var idx = _.findIndex(fmts, function( fmt ) {
- return fmt.pre === cssf.pre && fmt.ext === 'html';
- });
- fmts[ idx ].css = cssf.data;
- fmts[ idx ].cssPath = cssf.path;
- });
-
- // Remove CSS files from the formats array
- fmts = fmts.filter( function( fmt) {
- return fmt.ext !== 'css';
- });
-
- return formatsHash;
- }
-
- function friendlyName( val ) {
- val = val.trim().toLowerCase();
- var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
- return friendly[val] || val;
- }
-
- module.exports = Theme;
-
-}());
diff --git a/src/eng/generic-helpers.js b/src/eng/generic-helpers.js
deleted file mode 100644
index ab9c1f68..00000000
--- a/src/eng/generic-helpers.js
+++ /dev/null
@@ -1,169 +0,0 @@
-/**
-Generic template helper definitions for FluentCV.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module generic-helpers.js
-*/
-
-
-(function() {
-
- var MD = require('marked')
- , H2W = require('../utils/html-to-wpml')
- , moment = require('moment')
- , _ = require('underscore');
-
- /**
- Generic template helper function definitions.
- @class GenericHelpers
- */
- var GenericHelpers = module.exports = {
-
- /**
- Convert the input date to a specified format through Moment.js.
- @method formatDate
- */
- formatDate: function(datetime, format) {
- return moment ? moment( datetime ).format( format ) : datetime;
- },
-
- /**
- Convert inline Markdown to inline WordProcessingML.
- @method wpml
- */
- wpml: function( txt, inline ) {
- if(!txt) return '';
- inline = (inline && !inline.hash) || false;
- txt = inline ?
- MD(txt.trim()).replace(/^\s*
|<\/p>\s*$/gi, '') :
- MD(txt.trim());
- txt = H2W( txt.trim() );
- return txt;
- },
-
- /**
- Emit a conditional link.
- @method link
- */
- link: function( text, url ) {
- return url && url.trim() ?
- ('' + text + '') : text;
- },
-
- /**
- Return the last word of the specified text.
- @method lastWord
- */
- lastWord: function( txt ) {
- return txt && txt.trim() ? _.last( txt.split(' ') ) : '';
- },
-
- /**
- Convert a skill level to an RGB color triplet.
- @method skillColor
- @param lvl Input skill level. Skill level can be expressed as a string
- ("beginner", "intermediate", etc.), as an integer (1,5,etc), as a string
- integer ("1", "5", etc.), or as an RRGGBB color triplet ('#C00000',
- '#FFFFAA').
- */
- skillColor: function( lvl ) {
- var idx = skillLevelToIndex( lvl );
- var skillColors = (this.theme && this.theme.palette &&
- this.theme.palette.skillLevels) ||
- [ '#FFFFFF', '#5CB85C', '#F1C40F', '#428BCA', '#C00000' ];
- return skillColors[idx];
- },
-
- /**
- Return an appropriate height.
- @method lastWord
- */
- skillHeight: function( lvl ) {
- var idx = skillLevelToIndex( lvl );
- return ['38.25', '30', '16', '8', '0'][idx];
- },
-
- /**
- Return all but the last word of the input text.
- @method initialWords
- */
- initialWords: function( txt ) {
- return txt && txt.trim() ? _.initial( txt.split(' ') ).join(' ') : '';
- },
-
- /**
- Trim the protocol (http or https) from a URL/
- @method trimURL
- */
- trimURL: function( url ) {
- return url && url.trim() ? url.trim().replace(/^https?:\/\//i, '') : '';
- },
-
- /**
- Convert text to lowercase.
- @method toLower
- */
- toLower: function( txt ) {
- return txt && txt.trim() ? txt.toLowerCase() : '';
- },
-
- /**
- Return true if either value is truthy.
- @method either
- */
- either: function( lhs, rhs, options ) {
- if (lhs || rhs) return options.fn(this);
- },
-
- /**
- Perform a generic comparison.
- See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates
- @method compare
- */
- 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);
- }
-
- };
-
- function skillLevelToIndex( lvl ) {
- var idx = 0;
- if( String.is( lvl ) ) {
- lvl = lvl.trim().toLowerCase();
- var intVal = parseInt( lvl );
- if( isNaN( intVal ) ) {
- switch( lvl ) {
- case 'beginner': idx = 1; break;
- case 'intermediate': idx = 2; break;
- case 'advanced': idx = 3; break;
- case 'master': idx = 4; break;
- }
- }
- else {
- idx = Math.min( intVal / 2, 4 );
- idx = Math.max( 0, idx );
- }
- }
- else {
- idx = Math.min( lvl / 2, 4 );
- idx = Math.max( 0, idx );
- }
- return idx;
- }
-
-}());
diff --git a/src/eng/handlebars-generator.js b/src/eng/handlebars-generator.js
deleted file mode 100644
index b244a3fc..00000000
--- a/src/eng/handlebars-generator.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
-Definition of the HandlebarsGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module handlebars-generator.js
-*/
-
-(function() {
-
-
-
- var _ = require('underscore')
- , HANDLEBARS = require('handlebars')
- , FS = require('fs')
- , registerHelpers = require('./handlebars-helpers');
-
-
-
- /**
- Perform template-based resume generation using Handlebars.js.
- @class HandlebarsGenerator
- */
- var HandlebarsGenerator = module.exports = {
-
- generate: function( json, jst, format, cssInfo, opts, theme ) {
-
- // Pre-compile any partials present in the theme.
- _.each( theme.partials, function( el ) {
- var tplData = FS.readFileSync( el.path, 'utf8' );
- var compiledTemplate = HANDLEBARS.compile( tplData );
- HANDLEBARS.registerPartial( el.name, compiledTemplate );
- });
-
- // Register necessary helpers.
- registerHelpers( theme );
-
- // Compile and run the Handlebars template.
- var template = HANDLEBARS.compile(jst);
- return template({
- r: format === 'html' || format === 'pdf' ? json.markdownify() : json,
- RAW: json,
- filt: opts.filters,
- cssInfo: cssInfo,
- headFragment: opts.headFragment || ''
- });
-
- }
-
- };
-
-}());
diff --git a/src/eng/handlebars-helpers.js b/src/eng/handlebars-helpers.js
deleted file mode 100644
index d975c4e4..00000000
--- a/src/eng/handlebars-helpers.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
-Template helper definitions for Handlebars.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module handlebars-helpers.js
-*/
-
-
-(function() {
-
- var HANDLEBARS = require('handlebars')
- , _ = require('underscore')
- , helpers = require('./generic-helpers');
-
- /**
- Register useful Handlebars helpers.
- @method registerHelpers
- */
- module.exports = function( theme ) {
-
- helpers.theme = theme;
- HANDLEBARS.registerHelper( helpers );
-
- };
-
-}());
diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js
deleted file mode 100644
index fe2fb36e..00000000
--- a/src/eng/underscore-generator.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
-Definition of the UnderscoreGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module underscore-generator.js
-*/
-
-(function() {
-
-
-
- var _ = require('underscore');
-
-
-
- /**
- Perform template-based resume generation using Underscore.js.
- @class UnderscoreGenerator
- */
- var UnderscoreGenerator = module.exports = {
-
- generate: function( json, jst, format, cssInfo, opts, theme ) {
-
- // Tweak underscore's default template delimeters
- var delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template;
- if( opts.themeObj && opts.themeObj.delimeters ) {
- delims = _.mapObject( delims, function(val,key) {
- return new RegExp( val, "ig");
- });
- }
- _.templateSettings = delims;
-
- // Strip {# comments #}
- jst = jst.replace( delims.comment, '');
-
- // Compile and run the template. TODO: avoid unnecessary recompiles.
- var compiled = _.template(jst);
- var ret = compiled({
- r: format === 'html' || format === 'pdf' ? json.markdownify() : json,
- filt: opts.filters,
- XML: require('xml-escape'),
- RAW: json,
- cssInfo: cssInfo,
- headFragment: opts.headFragment || ''
- });
- return ret;
- }
-
- };
-
-
-
-}());
diff --git a/src/gen/base-generator.js b/src/gen/base-generator.js
deleted file mode 100644
index 9e1bc899..00000000
--- a/src/gen/base-generator.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
-Definition of the BaseGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module base-generator.js
-*/
-
-(function() {
-
- // Use J. Resig's nifty class implementation
- var Class = require( '../utils/class' );
-
- /**
- The BaseGenerator class is the root of the generator hierarchy. Functionality
- common to ALL generators lives here.
- */
-
- var BaseGenerator = module.exports = Class.extend({
-
- /**
- Base-class initialize.
- */
- init: function( outputFormat ) {
- this.format = outputFormat;
- },
-
- /**
- Status codes.
- */
- codes: {
- success: 0,
- themeNotFound: 1,
- copyCss: 2,
- resumeNotFound: 3,
- missingCommand: 4,
- invalidCommand: 5
- },
-
- /**
- Generator options.
- */
- opts: {
-
- }
-
- });
-}());
diff --git a/src/gen/html-generator.js b/src/gen/html-generator.js
deleted file mode 100644
index 8b82a3df..00000000
--- a/src/gen/html-generator.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
-Definition of the HTMLGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module html-generator.js
-*/
-
-(function() {
-
- var TemplateGenerator = require('./template-generator')
- , FS = require('fs-extra')
- , HTML = require( 'html' )
- , PATH = require('path');
-
- var HtmlGenerator = module.exports = TemplateGenerator.extend({
-
- init: function() {
- this._super( 'html' );
- },
-
- /**
- Copy satellite CSS files to the destination and optionally pretty-print
- the HTML resume prior to saving.
- */
- onBeforeSave: function( info ) {
- return this.opts.prettify ?
- HTML.prettyPrint( info.mk, this.opts.prettify ) : info.mk;
- }
-
- });
-
-}());
diff --git a/src/gen/html-pdf-generator.js b/src/gen/html-pdf-generator.js
deleted file mode 100644
index 853de90b..00000000
--- a/src/gen/html-pdf-generator.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
-Definition of the HtmlPdfGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module html-pdf-generator.js
-*/
-
-(function() {
-
- var TemplateGenerator = require('./template-generator')
- , FS = require('fs-extra')
- , HTML = require( 'html' );
-
- /**
- An HTML-based PDF resume generator for HackMyResume.
- */
- var HtmlPdfGenerator = module.exports = TemplateGenerator.extend({
-
- init: function() {
- this._super( 'pdf', 'html' );
- },
-
- /**
- Generate the binary PDF.
- */
- onBeforeSave: function( info ) {
- pdf( info.mk, info.outputFile );
- return null; // halt further processing
- }
-
- });
-
- /**
- Generate a PDF from HTML.
- */
- function pdf( markup, fOut ) {
-
- var pdfCount = 0;
- if( false ) { //( _opts.pdf === 'phantom' || _opts.pdf == 'all' ) {
- pdfCount++;
- require('phantom').create( function( ph ) {
- ph.createPage( function( page ) {
- page.setContent( markup );
- page.set('paperSize', {
- format: 'A4',
- orientation: 'portrait',
- margin: '1cm'
- });
- page.set("viewportSize", {
- width: 1024, // TODO: option-ify
- height: 768 // TODO: Use "A" sizes
- });
- page.set('onLoadFinished', function(success) {
- page.render( fOut );
- pdfCount++;
- ph.exit();
- });
- },
- { dnodeOpts: { weak: false } } );
- });
- }
- if( true ) { // _opts.pdf === 'wkhtmltopdf' || _opts.pdf == 'all' ) {
- var fOut2 = fOut;
- if( pdfCount == 1 ) {
- fOut2 = fOut2.replace(/\.pdf$/g, '.b.pdf');
- }
- require('wkhtmltopdf')( markup, { pageSize: 'letter' } )
- .pipe( FS.createWriteStream( fOut2 ) );
- pdfCount++;
- }
- }
-
-}());
diff --git a/src/gen/json-generator.js b/src/gen/json-generator.js
deleted file mode 100644
index 9e884e4b..00000000
--- a/src/gen/json-generator.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
-Definition of the JsonGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module json-generator.js
-*/
-
-var BaseGenerator = require('./base-generator');
-var FS = require('fs');
-var _ = require('underscore');
-
-/**
-The JsonGenerator generates a JSON resume directly.
-*/
-var JsonGenerator = module.exports = BaseGenerator.extend({
-
- init: function(){
- this._super( 'json' );
- },
-
- invoke: function( rez ) {
- // TODO: merge with FCVD
- function replacer( key,value ) { // Exclude these keys from stringification
- return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
- 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
- 'isModified', 'htmlPreview', 'safe' ],
- function( val ) { return key.trim() === val; }
- ) ? undefined : value;
- }
- return JSON.stringify( rez, replacer, 2 );
- },
-
- generate: function( rez, f ) {
- FS.writeFileSync( f, this.invoke(rez), 'utf8' );
- }
-
-});
diff --git a/src/gen/json-yaml-generator.js b/src/gen/json-yaml-generator.js
deleted file mode 100644
index 325cde76..00000000
--- a/src/gen/json-yaml-generator.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
-Definition of the JsonYamlGenerator class.
-@module json-yaml-generator.js
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-*/
-
-(function() {
-
- var BaseGenerator = require('./base-generator');
- var FS = require('fs');
- var YAML = require('yamljs');
-
- /**
- JsonYamlGenerator takes a JSON resume object and translates it directly to
- JSON without a template, producing an equivalent YAML-formatted resume. See
- also YamlGenerator (yaml-generator.js).
- */
-
- var JsonYamlGenerator = module.exports = BaseGenerator.extend({
-
- init: function(){
- this._super( 'yml' );
- },
-
- invoke: function( rez, themeMarkup, cssInfo, opts ) {
- return YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 );
- },
-
- generate: function( rez, f, opts ) {
- var data = YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 );
- FS.writeFileSync( f, data, 'utf8' );
- }
-
-
- });
-
-}());
diff --git a/src/gen/latex-generator.js b/src/gen/latex-generator.js
deleted file mode 100644
index 20e4a51e..00000000
--- a/src/gen/latex-generator.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
-Definition of the LaTeXGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module latex-generator.js
-*/
-
-var TemplateGenerator = require('./template-generator');
-
-/**
-LaTeXGenerator generates a LaTeX resume via TemplateGenerator.
-*/
-var LaTeXGenerator = module.exports = TemplateGenerator.extend({
-
- init: function(){
- this._super( 'latex', 'tex' );
- }
-
-});
diff --git a/src/gen/markdown-generator.js b/src/gen/markdown-generator.js
deleted file mode 100644
index a93d383e..00000000
--- a/src/gen/markdown-generator.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
-Definition of the MarkdownGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module markdown-generator.js
-*/
-
-var TemplateGenerator = require('./template-generator');
-
-/**
-MarkdownGenerator generates a Markdown-formatted resume via TemplateGenerator.
-*/
-var MarkdownGenerator = module.exports = TemplateGenerator.extend({
-
- init: function(){
- this._super( 'md', 'txt' );
- }
-
-});
diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js
deleted file mode 100644
index 86687e82..00000000
--- a/src/gen/template-generator.js
+++ /dev/null
@@ -1,298 +0,0 @@
-/**
-Definition of the TemplateGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module template-generator.js
-*/
-
-(function() {
-
-
-
- var FS = require( 'fs-extra' )
- , _ = require( 'underscore' )
- , MD = require( 'marked' )
- , XML = require( 'xml-escape' )
- , PATH = require('path')
- , parsePath = require('parse-filepath')
- , MKDIRP = require('mkdirp')
- , BaseGenerator = require( './base-generator' )
- , EXTEND = require('../utils/extend')
- , Theme = require('../core/theme');
-
-
-
- // Default options.
- var _defaultOpts = {
- engine: 'underscore',
- keepBreaks: true,
- freezeBreaks: false,
- nSym: '&newl;', // newline entity
- rSym: '&retn;', // return entity
- template: {
- interpolate: /\{\{(.+?)\}\}/g,
- escape: /\{\{\=(.+?)\}\}/g,
- evaluate: /\{\%(.+?)\%\}/g,
- comment: /\{\#(.+?)\#\}/g
- },
- filters: {
- out: function( txt ) { return txt; },
- raw: function( txt ) { return txt; },
- xml: function( txt ) { return XML(txt); },
- md: function( txt ) { return MD( txt || '' ); },
- mdin: function( txt ) {
- return MD(txt || '' ).replace(/^\s*
|<\/p>\s*$/gi, '');
- },
- lower: function( txt ) { return txt.toLowerCase(); },
- link: function( name, url ) { return url ?
- '' + name + '' : name; }
- },
- prettify: { // ← See https://github.com/beautify-web/js-beautify#options
- indent_size: 2,
- unformatted: ['em','strong','a'],
- max_char: 80, // ← See lib/html.js in above-linked repo
- //wrap_line_length: 120, <-- Don't use this
- }
- };
-
-
-
- /**
- TemplateGenerator performs resume generation via local Handlebar or Underscore
- style template expansion and is appropriate for text-based formats like HTML,
- plain text, and XML versions of Microsoft Word, Excel, and OpenOffice.
- @class TemplateGenerator
- */
- var TemplateGenerator = module.exports = BaseGenerator.extend({
-
-
-
- init: function( outputFormat, templateFormat, cssFile ){
- this._super( outputFormat );
- this.tplFormat = templateFormat || outputFormat;
- },
-
-
- /**
- String-based template generation method.
- @method invoke
- @param rez A FreshResume object.
- @param opts Generator options.
- @returns An array of strings representing generated output files.
- */
- invoke: function( rez, opts ) {
-
- // Carry over options
- this.opts = EXTEND( true, { }, _defaultOpts, opts );
-
- // Load the theme
- var themeInfo = themeFromMoniker.call( this );
- var theme = themeInfo.theme;
- var tFolder = themeInfo.folder;
- var tplFolder = PATH.join( tFolder, 'src' );
- var curFmt = theme.getFormat( this.format );
- var that = this;
-
- // "Generate": process individual files within the theme
- return {
- files: curFmt.files.map( function( tplInfo ) {
- return {
- info: tplInfo,
- data: tplInfo.action === 'transform' ?
- transform.call( that, rez, tplInfo, theme ) : undefined
- };
- }).filter(function(item){ return item !== null; }),
- themeInfo: themeInfo
- };
-
- },
-
-
-
- /**
- File-based template generation method.
- @method generate
- @param rez A FreshResume object.
- @param f Full path to the output resume file to generate.
- @param opts Generator options.
- */
- generate: function( rez, f, opts ) {
-
- // Call the generation method
- var genInfo = this.invoke( rez, opts );
-
- // Carry over options
- this.opts = EXTEND( true, { }, _defaultOpts, opts );
-
- // Load the theme
- var themeInfo = genInfo.themeInfo;
- var theme = themeInfo.theme;
- var tFolder = themeInfo.folder;
- var tplFolder = PATH.join( tFolder, 'src' );
- var outFolder = parsePath(f).dirname;
- var curFmt = theme.getFormat( this.format );
- var that = this;
-
- // "Generate": process individual files within the theme
- genInfo.files.forEach(function( file ){
-
- var thisFilePath;
-
- if( file.info.action === 'transform' ) {
- thisFilePath = PATH.join( outFolder, file.info.orgPath );
- try {
- if( that.onBeforeSave ) {
- file.data = that.onBeforeSave({
- theme: theme,
- outputFile: (file.info.major ? f : thisFilePath),
- mk: file.data
- });
- if( !file.data ) return; // PDF etc
- }
- var fileName = file.info.major ? f : thisFilePath;
- MKDIRP.sync( PATH.dirname( fileName ) );
- FS.writeFileSync( fileName, file.data,
- { encoding: 'utf8', flags: 'w' } );
- that.onAfterSave && that.onAfterSave(
- { outputFile: fileName, mk: file.data } );
- }
- catch(ex) {
- console.log(ex);
- }
- }
- else if( file.info.action === null/* && theme.explicit*/ ) {
- thisFilePath = PATH.join( outFolder, file.info.orgPath );
- try {
- MKDIRP.sync( PATH.dirname(thisFilePath) );
- FS.copySync( file.info.path, thisFilePath );
- }
- catch(ex) {
- console.log(ex);
- }
- }
- });
-
- // Some themes require a symlink structure. If so, create it.
- if( curFmt.symLinks ) {
- Object.keys( curFmt.symLinks ).forEach( function(loc) {
- var absLoc = PATH.join(outFolder, loc);
- var absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
- // 'file', 'dir', or 'junction' (Windows only)
- var type = parsePath( absLoc ).extname ? 'file' : 'junction';
- FS.symlinkSync( absTarg, absLoc, type);
- });
- }
-
- },
-
-
-
- /**
- Perform a single resume JSON-to-DEST resume transformation.
- @param json A FRESH or JRS resume object.
- @param jst The stringified template data
- @param format The format name, such as "html" or "latex"
- @param cssInfo Needs to be refactored.
- @param opts Options and passthrough data.
- */
- single: function( json, jst, format, cssInfo, opts, theme ) {
- this.opts.freezeBreaks && ( jst = freeze(jst) );
-
- var eng = require( '../eng/' + theme.engine + '-generator' );
- var result = eng.generate( json, jst, format, cssInfo, opts, theme );
-
- this.opts.freezeBreaks && ( result = unfreeze(result) );
- return result;
- }
-
-
- });
-
-
-
- /**
- Export the TemplateGenerator function/ctor.
- */
- module.exports = TemplateGenerator;
-
-
-
- /**
- Given a theme title, load the corresponding theme.
- */
- function themeFromMoniker() {
- // Verify the specified theme name/path
- var tFolder = PATH.join(
- parsePath( require.resolve('fluent-themes') ).dirname,
- this.opts.theme
- );
- var exists = require('path-exists').sync;
- if( !exists( tFolder ) ) {
- tFolder = PATH.resolve( this.opts.theme );
- if( !exists( tFolder ) ) {
- throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme};
- }
- }
-
- var t = this.opts.themeObj || new Theme().open( tFolder );
-
- // Load the theme and format
- return {
- theme: t,
- folder: tFolder
- };
- }
-
-
-
- function transform( rez, tplInfo, theme ) {
- try {
- var cssInfo = {
- file: tplInfo.css ? tplInfo.cssPath : null,
- data: tplInfo.css || null
- };
-
- return this.single( rez, tplInfo.data, this.format, cssInfo, this.opts,
- theme );
- }
- catch(ex) {
- console.log(ex);
- }
- }
-
-
-
- /**
- Freeze newlines for protection against errant JST parsers.
- */
- function freeze( markup ) {
- return markup
- .replace( _reg.regN, _defaultOpts.nSym )
- .replace( _reg.regR, _defaultOpts.rSym );
- }
-
-
-
- /**
- Unfreeze newlines when the coast is clear.
- */
- function unfreeze( markup ) {
- return markup
- .replace( _reg.regSymR, '\r' )
- .replace( _reg.regSymN, '\n' );
- }
-
-
-
- /**
- Regexes for linebreak preservation.
- */
- var _reg = {
- regN: new RegExp( '\n', 'g' ),
- regR: new RegExp( '\r', 'g' ),
- regSymN: new RegExp( _defaultOpts.nSym, 'g' ),
- regSymR: new RegExp( _defaultOpts.rSym, 'g' )
- };
-
-
-
-}());
diff --git a/src/gen/text-generator.js b/src/gen/text-generator.js
deleted file mode 100644
index d3611453..00000000
--- a/src/gen/text-generator.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
-Definition of the TextGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module text-generator.js
-*/
-
-var TemplateGenerator = require('./template-generator');
-
-/**
-The TextGenerator generates a plain-text resume via the TemplateGenerator.
-*/
-var TextGenerator = TemplateGenerator.extend({
-
- init: function(){
- this._super( 'txt' );
- },
-
-});
-
-module.exports = TextGenerator;
diff --git a/src/gen/word-generator.js b/src/gen/word-generator.js
deleted file mode 100644
index 1e15f3d1..00000000
--- a/src/gen/word-generator.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
-Definition of the WordGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module word-generator.js
-*/
-
-(function() {
-
- var TemplateGenerator = require('./template-generator');
- var WordGenerator = module.exports = TemplateGenerator.extend({
-
- init: function(){
- this._super( 'doc', 'xml' );
- }
-
- });
-
-
-}());
diff --git a/src/gen/xml-generator.js b/src/gen/xml-generator.js
deleted file mode 100644
index 04146131..00000000
--- a/src/gen/xml-generator.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
-Definition of the XMLGenerator class.
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-@module xml-generator.js
-*/
-
-var BaseGenerator = require('./base-generator');
-
-/**
-The XmlGenerator generates an XML resume via the TemplateGenerator.
-*/
-var XMLGenerator = module.exports = BaseGenerator.extend({
-
- init: function(){
- this._super( 'xml' );
- },
-
-});
diff --git a/src/gen/yaml-generator.js b/src/gen/yaml-generator.js
deleted file mode 100644
index bb9f9953..00000000
--- a/src/gen/yaml-generator.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
-Definition of the YAMLGenerator class.
-@module yaml-generator.js
-@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
-*/
-
-
-(function() {
-
- var TemplateGenerator = require('./template-generator');
-
- /**
- YamlGenerator generates a YAML-formatted resume via TemplateGenerator.
- */
-
- var YAMLGenerator = module.exports = TemplateGenerator.extend({
-
- init: function(){
- this._super( 'yml', 'yml' );
- }
-
- });
-
-}());
diff --git a/src/generators/base-generator.js b/src/generators/base-generator.js
new file mode 100644
index 00000000..d271dde1
--- /dev/null
+++ b/src/generators/base-generator.js
@@ -0,0 +1,37 @@
+/*
+ * decaffeinate suggestions:
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+/**
+Definition of the BaseGenerator class.
+@module generators/base-generator
+@license MIT. See LICENSE.md for details.
+*/
+
+
+/**
+The BaseGenerator class is the root of the generator hierarchy. Functionality
+common to ALL generators lives here.
+*/
+
+let BaseGenerator;
+module.exports = (BaseGenerator = (function() {
+ BaseGenerator = class BaseGenerator {
+ static initClass() {
+
+ /** Status codes. */
+ this.prototype.codes = require('../core/status-codes');
+
+ /** Generator options. */
+ this.prototype.opts = { };
+ }
+
+ /** Base-class initialize. */
+ constructor( format ) {
+ this.format = format;
+ }
+ };
+ BaseGenerator.initClass();
+ return BaseGenerator;
+})());
diff --git a/src/generators/html-generator.js b/src/generators/html-generator.js
new file mode 100644
index 00000000..8089f38c
--- /dev/null
+++ b/src/generators/html-generator.js
@@ -0,0 +1,39 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+/**
+Definition of the HTMLGenerator class.
+@module generators/html-generator
+@license MIT. See LICENSE.md for details.
+*/
+
+
+
+const TemplateGenerator = require('./template-generator');
+const HTML = require('html');
+require('string.prototype.endswith');
+
+
+
+class HtmlGenerator extends TemplateGenerator {
+
+ constructor() { super('html'); }
+
+ /**
+ Copy satellite CSS files to the destination and optionally pretty-print
+ the HTML resume prior to saving.
+ */
+ onBeforeSave( info ) {
+ if (info.outputFile.endsWith('.css')) {
+ return info.mk;
+ }
+ if (this.opts.prettify) {
+ return HTML.prettyPrint(info.mk, this.opts.prettify);
+ } else { return info.mk; }
+ }
+}
+
+
+module.exports = HtmlGenerator;
diff --git a/src/generators/html-pdf-cli-generator.js b/src/generators/html-pdf-cli-generator.js
new file mode 100644
index 00000000..d5adcd8f
--- /dev/null
+++ b/src/generators/html-pdf-cli-generator.js
@@ -0,0 +1,129 @@
+/*
+ * decaffeinate suggestions:
+ * DS103: Rewrite code to no longer use __guard__
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+/**
+Definition of the HtmlPdfCLIGenerator class.
+@module generators/html-pdf-generator.js
+@license MIT. See LICENSE.md for details.
+*/
+
+
+
+const TemplateGenerator = require('./template-generator');
+const FS = require('fs-extra');
+const PATH = require('path');
+const SLASH = require('slash');
+const _ = require('underscore');
+const HMSTATUS = require('../core/status-codes');
+const SPAWN = require('../utils/safe-spawn');
+
+
+/**
+An HTML-driven PDF resume generator for HackMyResume. Talks to Phantom,
+wkhtmltopdf, and other PDF engines over a CLI (command-line interface).
+If an engine isn't installed for a particular platform, error out gracefully.
+*/
+
+class HtmlPdfCLIGenerator extends TemplateGenerator {
+
+
+
+ constructor() { super('pdf', 'html'); }
+
+
+
+ /** Generate the binary PDF. */
+ onBeforeSave( info ) {
+ //console.dir _.omit( info, 'mk' ), depth: null, colors: true
+ if ((info.ext !== 'html') && (info.ext !== 'pdf')) { return info.mk; }
+ let safe_eng = info.opts.pdf || 'wkhtmltopdf';
+ if (safe_eng === 'phantom') { safe_eng = 'phantomjs'; }
+ if (_.has(engines, safe_eng)) {
+ this.errHandler = info.opts.errHandler;
+ engines[ safe_eng ].call(this, info.mk, info.outputFile, info.opts, this.onError);
+ return null; // halt further processing
+ }
+ }
+
+
+
+ /* Low-level error callback for spawn(). May be called after HMR process
+ termination, so object references may not be valid here. That's okay; if
+ the references are invalid, the error was already logged. We could use
+ spawn-watch here but that causes issues on legacy Node.js. */
+ onError(ex, param) {
+ __guardMethod__(param.errHandler, 'err', o => o.err(HMSTATUS.pdfGeneration, ex));
+ }
+}
+
+module.exports = HtmlPdfCLIGenerator;
+
+// TODO: Move each engine to a separate module
+var engines = {
+
+
+
+ /**
+ Generate a PDF from HTML using wkhtmltopdf's CLI interface.
+ Spawns a child process with `wkhtmltopdf `. wkhtmltopdf
+ must be installed and path-accessible.
+ TODO: If HTML generation has run, reuse that output
+ TODO: Local web server to ease wkhtmltopdf rendering
+ */
+ wkhtmltopdf(markup, fOut, opts, on_error) {
+ // Save the markup to a temporary file
+ const tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
+ FS.writeFileSync(tempFile, markup, 'utf8');
+
+ // Prepare wkhtmltopdf arguments.
+ let wkopts = _.extend({'margin-top': '10mm', 'margin-bottom': '10mm'}, opts.wkhtmltopdf);
+ wkopts = _.flatten(_.map(wkopts, (v, k) => [`--${k}`, v]));
+ const wkargs = wkopts.concat([ tempFile, fOut ]);
+
+ SPAWN('wkhtmltopdf', wkargs , false, on_error, this);
+ },
+
+
+
+ /**
+ Generate a PDF from HTML using Phantom's CLI interface.
+ Spawns a child process with `phantomjs