From 2a8f0196b4710f8c64ce1b3e01cf1e3d9f0ecab3 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Tue, 1 Dec 2015 14:33:49 -0500 Subject: [PATCH 01/29] Update LICENSE author. --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index e095e65..b85e75a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ The MIT License =============== -Copyright (c) 2015 James M. Devlin (https://github.com/devlinjd) +Copyright (c) 2015 James M. Devlin (https://github.com/hacksalot) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 2431ae4d8963912ad452a5c61f8305115df69cae Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 2 Dec 2015 14:56:06 -0500 Subject: [PATCH 02/29] Bump version to 0.10.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9536d03..2c3e444 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fluentcv", - "version": "0.9.1", + "version": "0.10.0", "description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.", "repository": { "type": "git", From 5b3a25c4612f202ed90493179acd65e61b11fb64 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 2 Dec 2015 14:56:36 -0500 Subject: [PATCH 03/29] Support NEW command. --- README.md | 9 ++ src/core/empty-fresh.json | 182 ++++++++++++++++++++++++ src/core/{empty.json => empty-jrs.json} | 0 src/core/fresh-resume.js | 11 +- src/core/jrs-resume.js | 4 +- src/fluentcmd.js | 17 ++- src/index.js | 12 +- src/use.txt | 12 +- 8 files changed, 227 insertions(+), 20 deletions(-) create mode 100644 src/core/empty-fresh.json rename src/core/{empty.json => empty-jrs.json} (100%) diff --git a/README.md b/README.md index 704c16e..e34a1a6 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,15 @@ it when you need to submit, upload, print, or email resumes in specific formats. fluentcv BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all ``` +- `new` creates a new resume in FRESH or JSON Resume format. + + ```bash + # fluentcv NEW [-f ] + fluentcv NEW resume.json + fluentcv NEW resume.json -f fresh + fluentcv NEW r1.json r2.json -f jrs + ``` + - `convert` converts your source resume between FRESH and JSON Resume formats. Use it to convert between the two formats to take advantage of tools and services. diff --git a/src/core/empty-fresh.json b/src/core/empty-fresh.json new file mode 100644 index 0000000..347def5 --- /dev/null +++ b/src/core/empty-fresh.json @@ -0,0 +1,182 @@ +{ + + "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": "", + "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": "", + "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/empty.json b/src/core/empty-jrs.json similarity index 100% rename from src/core/empty.json rename to src/core/empty-jrs.json diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index fafe024..fb647ef 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -143,9 +143,10 @@ Definition of the FRESHResume class. delete this.employment; delete this.service; delete this.education; - //delete this.awards; - delete this.publications; - //delete this.interests; + delete this.recognition; + delete this.reading; + delete this.writing; + delete this.interests; delete this.skills; delete this.social; }; @@ -154,7 +155,7 @@ Definition of the FRESHResume class. Get the default (empty) sheet. */ FreshResume.default = function() { - return new FreshResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); + return new FreshResume().open( PATH.join( __dirname, 'empty-fresh.json'), 'Empty' ); } /** @@ -243,7 +244,7 @@ Definition of the FRESHResume class. // return( a.safeDate.isBefore(b.safeDate) ) ? 1 // : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0; // }); - this.publications && this.publications.sort( function(a, b) { + 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; }); diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js index b7fed04..b7bfba3 100644 --- a/src/core/jrs-resume.js +++ b/src/core/jrs-resume.js @@ -50,7 +50,7 @@ Definition of the JRSResume class. */ JRSResume.prototype.stringify = function() { function replacer( key,value ) { // Exclude these keys from stringification - return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', + return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'], function( val ) { return key.trim() === val; } @@ -126,7 +126,7 @@ Definition of the JRSResume class. Get the default (empty) sheet. */ JRSResume.default = function() { - return new JRSResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); + return new JRSResume().open( PATH.join( __dirname, 'empty-jrs.json'), 'Empty' ); } /** diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 2a329ba..b836d5d 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -104,7 +104,7 @@ module.exports = function () { var theFormat = _fmts.filter( function( fmt ) { return fmt.name === fi.fmt.pre; })[0]; - MKDIRP.sync( path.dirname(fOut) ); // Ensure dest folder exists; + MKDIRP.sync( path.dirname( fOut ) ); // Ensure dest folder exists; theFormat.gen.generate( rez, fOut, _opts ); } catch( ex ) { @@ -227,6 +227,20 @@ module.exports = function () { }); } + /** + Create a new empty resume in either FRESH or JRS format. + */ + function create( src, dst, opts, logger ) { + _log = logger || console.log; + dst = src || ['resume.json']; + dst.forEach( function( t ) { + var safeFormat = opts.format.toUpperCase(); + _log('Creating '.useful +safeFormat.useful.bold+ ' resume: '.useful + t.useful.bold); + MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists; + FLUENT[ safeFormat + 'Resume' ].default().save( t ); + }); + } + /** Display help documentation. */ @@ -275,6 +289,7 @@ module.exports = function () { build: generate, validate: validate, convert: convert, + new: create, help: help }, lib: require('./fluentlib'), diff --git a/src/index.js b/src/index.js index bb4b5f9..b5dd466 100644 --- a/src/index.js +++ b/src/index.js @@ -59,17 +59,14 @@ function main() { var splitAt = _.indexOf( params, 'to' ); if( splitAt === a._.length - 1 ) { // 'TO' cannot be the last argument - logMsg('Please '.warn + 'specify an output file'.warnBold + - ' for this operation or '.warn + 'omit the TO keyword'.warnBold + '.'.warn ); + logMsg('Please '.warn + 'specify an output file'.warn.bold + + ' for this operation or '.warn + 'omit the TO keyword'.warn.bold + + '.'.warn ); return; } var src = a._.slice(1, splitAt === -1 ? undefined : splitAt ); var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 ); - - // Preload our params array - //var dst = (a.o && ((typeof a.o === 'string' && [ a.o ]) || a.o)) || []; - //dst = (dst === true) ? [] : dst; // Handle -o with missing output file var parms = [ src, dst, opts, logMsg ]; // Invoke the action @@ -86,6 +83,7 @@ function getOpts( args ) { noPretty = noPretty && (noPretty === true || noPretty === 'true'); return { theme: args.t || 'modern', + format: args.f || 'FRESH', prettify: !noPretty, silent: args.s || args.silent }; @@ -105,7 +103,7 @@ function handleError( ex ) { Object.keys( FCMD.verbs ).map( function(v, idx, ar) { return (idx === ar.length - 1 ? 'or '.guide : '') + v.toUpperCase().guide; - }).join(', '.guide) + ") to get started.\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; + }).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; break; //case 4: msg = title + '\n' + ; break; case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break; diff --git a/src/use.txt b/src/use.txt index 1072347..69249c3 100644 --- a/src/use.txt +++ b/src/use.txt @@ -2,21 +2,23 @@ Usage: fluentcv [TO ] [-t ] - should be BUILD, CONVERT, VALIDATE, or HELP. should + should be BUILD, NEW, CONVERT, VALIDATE, or HELP. should be the path to one or more FRESH or JSON Resume format resumes. should be the name of the destination resume to be created, if any. The parameter should be the name of a predefined theme (for example: -COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a -custom theme. +COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a custom +theme. fluentcv BUILD resume.json TO out/resume.all + fluentcv NEW resume.json fluentcv CONVERT resume.json TO resume-jrs.json fluentcv VALIDATE resume.json Both SOURCES and TARGETS can accept multiple files: fluentCV BUILD r1.json r2.json TO out/resume.all out2/resume.html + fluentCV NEW r1.json r2.json r3.json fluentCV VALIDATE resume.json resume2.json resume3.json -See https://github.com/fluentdesk/fluentCV/blob/master/README.md -for more information. +See https://github.com/fluentdesk/fluentCV/blob/master/README.md for more +information. From 92ca11f23c72a644593530dc893add7e90cd6082 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 2 Dec 2015 15:10:38 -0500 Subject: [PATCH 04/29] Adjust output. --- src/fluentcmd.js | 2 +- src/use.txt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/fluentcmd.js b/src/fluentcmd.js index b836d5d..94b5b84 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -235,7 +235,7 @@ module.exports = function () { dst = src || ['resume.json']; dst.forEach( function( t ) { var safeFormat = opts.format.toUpperCase(); - _log('Creating '.useful +safeFormat.useful.bold+ ' resume: '.useful + t.useful.bold); + _log('Creating new '.useful +safeFormat.useful.bold+ ' resume: '.useful + t.useful.bold); MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists; FLUENT[ safeFormat + 'Resume' ].default().save( t ); }); diff --git a/src/use.txt b/src/use.txt index 69249c3..d943d28 100644 --- a/src/use.txt +++ b/src/use.txt @@ -1,13 +1,14 @@ Usage: - fluentcv [TO ] [-t ] + fluentcv [TO ] [-t ] [-f ] should be BUILD, NEW, CONVERT, VALIDATE, or HELP. should be the path to one or more FRESH or JSON Resume format resumes. should be the name of the destination resume to be created, if any. The parameter should be the name of a predefined theme (for example: COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a custom -theme. +theme. should be either FRESH (for a FRESH-format resume) or JRS +(for a JSON Resume-format resume). fluentcv BUILD resume.json TO out/resume.all fluentcv NEW resume.json From 263f224e1bd9f8e9fcfd343a056034e1c7eb6ffa Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 6 Dec 2015 05:50:14 -0500 Subject: [PATCH 05/29] Bump fluent-themes version to 0.6.0-beta. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c3e444..cfa1183 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "FRESCA": "fluentdesk/FRESCA#v0.1.0", "colors": "^1.1.2", - "fluent-themes": "0.5.1-beta", + "fluent-themes": "0.6.0-beta", "fs-extra": "^0.24.0", "html": "0.0.10", "is-my-json-valid": "^2.12.2", From e4d098a3ce52de38862c506418bdfa3acdb10ca9 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 6 Dec 2015 05:51:03 -0500 Subject: [PATCH 06/29] Add safety for implicit Markdown. --- src/gen/template-generator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 547579c..121402d 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -28,8 +28,8 @@ var _defaultOpts = { 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, ''); }, + 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 } From fb783cdbc607ca93e9692dfdf00aab578a3f9ad2 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 6 Dec 2015 14:57:20 -0500 Subject: [PATCH 07/29] Add Handlebars dependency. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cfa1183..91a401c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "colors": "^1.1.2", "fluent-themes": "0.6.0-beta", "fs-extra": "^0.24.0", + "handlebars": "^4.0.5", "html": "0.0.10", "is-my-json-valid": "^2.12.2", "jst": "0.0.13", From 3b8d100f390264ebc7b957b1d96e27d56e7a49ef Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 6 Dec 2015 16:19:55 -0500 Subject: [PATCH 08/29] Add baseline Handlebars support. --- src/core/theme.js | 9 +- src/eng/handlebars-generator.js | 18 ++ src/eng/underscore-generator.js | 37 ++++ src/gen/template-generator.js | 296 +++++++++++++++----------------- 4 files changed, 204 insertions(+), 156 deletions(-) create mode 100644 src/eng/handlebars-generator.js create mode 100644 src/eng/underscore-generator.js diff --git a/src/core/theme.js b/src/core/theme.js index 9ade483..b971f39 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -10,10 +10,11 @@ Abstract theme representation. , validator = require('is-my-json-valid') , _ = require('underscore') , PATH = require('path') + , EXTEND = require('../utils/extend') , moment = require('moment'); /** - The Theme class represents a specific presentation of a resume. + The Theme class is a representation of a FluentCV theme asset. @class Theme */ function Theme() { @@ -31,8 +32,12 @@ Abstract theme representation. return friendly[val] || val; } - // Remember the theme folder; might be custom + // Open the theme.json file; should have the same name as folder this.folder = themeFolder; + var pathInfo = PATH.parse( themeFolder ); + var themeFile = PATH.join( themeFolder, pathInfo.base + '.json' ); + var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) ); + EXTEND( true, this, themeInfo ); // Iterate over all files in the theme folder, producing an array, fmts, // containing info for each file. diff --git a/src/eng/handlebars-generator.js b/src/eng/handlebars-generator.js new file mode 100644 index 0000000..807c891 --- /dev/null +++ b/src/eng/handlebars-generator.js @@ -0,0 +1,18 @@ +/** +Handlebars template generate for FluentCV. +@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk. +*/ + +(function() { + + var _ = require('underscore'); + var HANDLEBARS = require('handlebars'); + + module.exports = function( json, jst, format, cssInfo, opts ) { + + var template = HANDLEBARS.compile(jst); + return template( { r: json, filt: opts.filters, cssInfo: cssInfo, headFragment: opts.headFragment || '' } ) + + }; + +}()); diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js new file mode 100644 index 0000000..f744153 --- /dev/null +++ b/src/eng/underscore-generator.js @@ -0,0 +1,37 @@ +/** +Underscore template generate for FluentCV. +@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk. +*/ + +(function() { + + var _ = require('underscore'); + + module.exports = function( json, jst, format, cssInfo, opts ) { + + // Tweak underscore's default template delimeters + _.templateSettings = opts.template; + + // Convert {{ someVar }} to {% print(filt.out(someVar) %} + // Convert {{ someVar|someFilter }} to {% print(filt.someFilter(someVar) %} + jst = jst.replace( _.templateSettings.interpolate, function replace(m, p1) { + if( p1.indexOf('|') > -1 ) { + var terms = p1.split('|'); + return '{% print( filt.' + terms[1] + '( ' + terms[0] + ' )) %}'; + } + else { + return '{% print( filt.out(' + p1 + ') ) %}'; + } + }); + + // Strip {# comments #} + jst = jst.replace( _.templateSettings.comment, ''); + + // Compile and run the template. TODO: avoid unnecessary recompiles. + jst = _.template(jst)({ r: json, filt: opts.filters, cssInfo: cssInfo, headFragment: opts.headFragment || '' }); + + return jst; + + }; + +}()); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 121402d..609b624 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -1,176 +1,164 @@ /** Template-based resume generator base for FluentCV. -@license Copyright (c) 2015 | James M. Devlin +@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk. */ -var FS = require( 'fs' ) - , _ = require( 'underscore' ) - , MD = require( 'marked' ) - , XML = require( 'xml-escape' ) - , PATH = require('path') - , BaseGenerator = require( './base-generator' ) - , EXTEND = require('../utils/extend') - , Theme = require('../core/theme'); +(function() { -// Default options. -var _defaultOpts = { - keepBreaks: true, - freezeBreaks: true, - 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 - } -}; + var FS = require( 'fs' ) + , _ = require( 'underscore' ) + , MD = require( 'marked' ) + , XML = require( 'xml-escape' ) + , PATH = require('path') + , BaseGenerator = require( './base-generator' ) + , EXTEND = require('../utils/extend') + , Theme = require('../core/theme'); -/** -TemplateGenerator performs resume generation via Underscore-style template -expansion and is appropriate for text-based formats like HTML, plain text, -and XML versions of Microsoft Word, Excel, and OpenOffice. -*/ -var TemplateGenerator = module.exports = BaseGenerator.extend({ - - /** outputFormat: html, txt, pdf, doc - templateFormat: html or txt - **/ - init: function( outputFormat, templateFormat, cssFile ){ - this._super( outputFormat ); - this.tplFormat = templateFormat || outputFormat; - }, - - /** Default generation method for template-based generators. */ - invoke: function( rez, themeMarkup, cssInfo, opts ) { - - // Compile and invoke the template! - this.opts = EXTEND( true, {}, _defaultOpts, opts ); - mk = this.single( rez, themeMarkup, this.format, cssInfo, { } ); - this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f )); - return mk; - - }, - - /** Default generation method for template-based generators. */ - generate: function( rez, f, opts ) { - - // Carry over options - this.opts = EXTEND( true, { }, _defaultOpts, opts ); - - // Verify the specified theme name/path - var tFolder = PATH.join( - PATH.parse( require.resolve('fluent-themes') ).dir, - this.opts.theme - ); - var exists = require('../utils/file-exists'); - if (!exists( tFolder )) { - tFolder = PATH.resolve( this.opts.theme ); - if (!exists( tFolder )) { - throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme }; - } + // Default options. + var _defaultOpts = { + engine: 'underscore', + keepBreaks: true, + freezeBreaks: true, + 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 } - - // Load the theme - var theme = opts.themeObj || new Theme().open( tFolder ); - - // Load theme and CSS data - var tplFolder = PATH.join( tFolder, 'templates' ); - var curFmt = theme.getFormat( this.format ); - var cssInfo = { file: curFmt.css ? curFmt.cssPath : null, data: curFmt.css || null }; - - // Compile and invoke the template! - var mk = this.single( rez, curFmt.data, this.format, cssInfo, opts ); - this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); - FS.writeFileSync( f, mk, { encoding: 'utf8', flags: 'w' } ); - - }, + }; /** - Perform a single resume JSON-to-DEST resume transformation. Exists as a - separate function in order to expose string-based transformations to clients - who don't have access to filesystem resources (in-browser, etc.). + 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. */ - single: function( json, jst, format, cssInfo, opts ) { + var TemplateGenerator = module.exports = BaseGenerator.extend({ - // Freeze whitespace in the template. - this.opts.freezeBreaks && ( jst = freeze(jst) ); + /** outputFormat: html, txt, pdf, doc + templateFormat: html or txt + **/ + init: function( outputFormat, templateFormat, cssFile ){ + this._super( outputFormat ); + this.tplFormat = templateFormat || outputFormat; + }, - // Tweak underscore's default template delimeters - _.templateSettings = this.opts.template; + /** Default generation method for template-based generators. */ + invoke: function( rez, themeMarkup, cssInfo, opts ) { - // Convert {{ someVar }} to {% print(filt.out(someVar) %} - // Convert {{ someVar|someFilter }} to {% print(filt.someFilter(someVar) %} - jst = jst.replace( _.templateSettings.interpolate, function replace(m, p1) { - if( p1.indexOf('|') > -1 ) { - var terms = p1.split('|'); - return '{% print( filt.' + terms[1] + '( ' + terms[0] + ' )) %}'; + // Compile and invoke the template! + this.opts = EXTEND( true, {}, _defaultOpts, opts ); + mk = this.single( rez, themeMarkup, this.format, cssInfo, { } ); + this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f )); + return mk; + + }, + + /** Default generation method for template-based generators. */ + generate: function( rez, f, opts ) { + + // Carry over options + this.opts = EXTEND( true, { }, _defaultOpts, opts ); + + // Verify the specified theme name/path + var tFolder = PATH.join( + PATH.parse( require.resolve('fluent-themes') ).dir, + this.opts.theme + ); + var exists = require('../utils/file-exists'); + if (!exists( tFolder )) { + tFolder = PATH.resolve( this.opts.theme ); + if (!exists( tFolder )) { + throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme }; + } } - else { - return '{% print( filt.out(' + p1 + ') ) %}'; - } - }); - // Strip {# comments #} - jst = jst.replace( _.templateSettings.comment, ''); + // Load the theme + var theme = opts.themeObj || new Theme().open( tFolder ); - // Compile and run the template. TODO: avoid unnecessary recompiles. - jst = _.template(jst)({ r: json, filt: this.opts.filters, cssInfo: cssInfo, headFragment: this.opts.headFragment || '' }); + // Load theme and CSS data + var tplFolder = PATH.join( tFolder, 'templates' ); + var curFmt = theme.getFormat( this.format ); + var cssInfo = { file: curFmt.css ? curFmt.cssPath : null, data: curFmt.css || null }; - // Unfreeze whitespace - this.opts.freezeBreaks && ( jst = unfreeze(jst) ); + // Compile and invoke the template! + var mk = this.single( rez, curFmt.data, this.format, cssInfo, this.opts ); + this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); + FS.writeFileSync( f, mk, { encoding: 'utf8', flags: 'w' } ); - return jst; + }, + + /** + Perform a single resume JSON-to-DEST resume transformation. Exists as a + separate function in order to expose string-based transformations to clients + who don't have access to filesystem resources (in-browser, etc.). + */ + single: function( json, jst, format, cssInfo, opts ) { + + // Freeze whitespace in the template. + this.opts.freezeBreaks && ( jst = freeze(jst) ); + + // Apply the template. + var eng = require( '../eng/' + opts.themeObj.engine + '-generator' ); + var result = eng( json, jst, format, cssInfo, opts ); + + // Unfreeze whitespace. + this.opts.freezeBreaks && ( result = unfreeze(result) ); + + return result; + } + + + }); + + /** + Export the TemplateGenerator function/ctor. + */ + module.exports = TemplateGenerator; + + /** + 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' ) + }; -/** -Export the TemplateGenerator function/ctor. -*/ -module.exports = TemplateGenerator; - -/** -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' ) -}; +}()); From 307c37dc44c2d98dd2e35824956515e83f7221e3 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 6 Dec 2015 18:18:36 -0500 Subject: [PATCH 09/29] Use "src" subfolder instead of "templates". --- src/core/theme.js | 2 +- src/gen/html-generator.js | 2 +- src/gen/template-generator.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/theme.js b/src/core/theme.js index b971f39..c910c65 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -41,7 +41,7 @@ Abstract theme representation. // Iterate over all files in the theme folder, producing an array, fmts, // containing info for each file. - var tplFolder = PATH.join( themeFolder, 'templates' ); + var tplFolder = PATH.join( themeFolder, 'src' ); var fmts = FS.readdirSync( tplFolder ).map( function( file ) { var absPath = PATH.join( tplFolder, file ); var pathInfo = PATH.parse(absPath); diff --git a/src/gen/html-generator.js b/src/gen/html-generator.js index b0f2992..a7740ad 100644 --- a/src/gen/html-generator.js +++ b/src/gen/html-generator.js @@ -21,7 +21,7 @@ HTML resume generator for FluentCV. the HTML resume prior to saving. */ onBeforeSave: function( info ) { - var cssSrc = PATH.join( info.theme.folder, 'templates', '*.css' ) + var cssSrc = PATH.join( info.theme.folder, 'src', '*.css' ) , outFolder = PATH.parse( info.outputFile ).dir, that = this; info.theme.cssFiles.forEach( function( f ) { diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 609b624..3f5b0d7 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -94,7 +94,7 @@ Template-based resume generator base for FluentCV. var theme = opts.themeObj || new Theme().open( tFolder ); // Load theme and CSS data - var tplFolder = PATH.join( tFolder, 'templates' ); + var tplFolder = PATH.join( tFolder, 'src' ); var curFmt = theme.getFormat( this.format ); var cssInfo = { file: curFmt.css ? curFmt.cssPath : null, data: curFmt.css || null }; From 228f14d06c72e5e125c21825e8863f0c9795631e Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 6 Dec 2015 18:19:33 -0500 Subject: [PATCH 10/29] Support recursive theme template loading. --- package.json | 1 + src/core/theme.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 91a401c..ee3a1b8 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "minimist": "^1.2.0", "mkdirp": "^0.5.1", "moment": "^2.10.6", + "recursive-readdir-sync": "^1.0.6", "underscore": "^1.8.3", "wkhtmltopdf": "^0.1.5", "xml-escape": "^1.0.0", diff --git a/src/core/theme.js b/src/core/theme.js index c910c65..3f5c0eb 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -11,7 +11,8 @@ Abstract theme representation. , _ = require('underscore') , PATH = require('path') , EXTEND = require('../utils/extend') - , moment = require('moment'); + , moment = require('moment') + , recursiveReadSync = require('recursive-readdir-sync'); /** The Theme class is a representation of a FluentCV theme asset. @@ -42,7 +43,7 @@ Abstract theme representation. // Iterate over all files in the theme folder, producing an array, fmts, // containing info for each file. var tplFolder = PATH.join( themeFolder, 'src' ); - var fmts = FS.readdirSync( tplFolder ).map( function( file ) { + var fmts = recursiveReadSync( tplFolder ).map( function( file ) { var absPath = PATH.join( tplFolder, file ); var pathInfo = PATH.parse(absPath); var temp = [ pathInfo.name, { From cf2562167923c97ca49d5b0577a8f3f48543a136 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Sun, 6 Dec 2015 18:29:16 -0500 Subject: [PATCH 11/29] Introduce placeholder LaTeX generator. --- src/fluentcmd.js | 3 ++- src/fluentlib.js | 3 ++- src/gen/latex-generator.js | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/gen/latex-generator.js diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 94b5b84..8e61ad6 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -265,7 +265,8 @@ module.exports = function () { { 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: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() }, + { name: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() } ]; /** diff --git a/src/fluentlib.js b/src/fluentlib.js index 26e23b7..f6e1260 100644 --- a/src/fluentlib.js +++ b/src/fluentlib.js @@ -16,5 +16,6 @@ module.exports = { MarkdownGenerator: require('./gen/markdown-generator'), JsonGenerator: require('./gen/json-generator'), YamlGenerator: require('./gen/yaml-generator'), - JsonYamlGenerator: require('./gen/json-yaml-generator') + JsonYamlGenerator: require('./gen/json-yaml-generator'), + LaTeXGenerator: require('./gen/latex-generator') }; diff --git a/src/gen/latex-generator.js b/src/gen/latex-generator.js new file mode 100644 index 0000000..43b6b67 --- /dev/null +++ b/src/gen/latex-generator.js @@ -0,0 +1,17 @@ +/** +LaTeX resume generator for FluentCV. +@license MIT. Copyright (c) 2015 James Devlin / FluentDesk +*/ + +var TemplateGenerator = require('./template-generator'); + +/** +LaTeXGenerator generates a LaTeX resume via TemplateGenerator. +*/ +var LaTeXGenerator = module.exports = TemplateGenerator.extend({ + + init: function(){ + this._super( 'tex', 'tex' ); + } + +}); From 5f19f0a7df0cd9606572d6a893231635139e5eca Mon Sep 17 00:00:00 2001 From: devlinjd Date: Mon, 7 Dec 2015 09:51:00 -0500 Subject: [PATCH 12/29] Add baseline support for multifile themes. #rough --- src/core/theme.js | 74 ++++++++++++++++++++++++----------- src/fluentcmd.js | 37 +++++++++++++----- src/gen/html-generator.js | 4 +- src/gen/template-generator.js | 14 ++++--- 4 files changed, 90 insertions(+), 39 deletions(-) diff --git a/src/core/theme.js b/src/core/theme.js index 3f5c0eb..3fbcb01 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -12,7 +12,7 @@ Abstract theme representation. , PATH = require('path') , EXTEND = require('../utils/extend') , moment = require('moment') - , recursiveReadSync = require('recursive-readdir-sync'); + , RECURSIVE_READ_DIR = require('recursive-readdir-sync'); /** The Theme class is a representation of a FluentCV theme asset. @@ -40,47 +40,77 @@ Abstract theme representation. var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) ); EXTEND( true, this, themeInfo ); + var formatsHash = { }; + // Iterate over all files in the theme folder, producing an array, fmts, // containing info for each file. var tplFolder = PATH.join( themeFolder, 'src' ); - var fmts = recursiveReadSync( tplFolder ).map( function( file ) { - var absPath = PATH.join( tplFolder, file ); - var pathInfo = PATH.parse(absPath); - var temp = [ pathInfo.name, { - title: friendlyName(pathInfo.name), - pre: pathInfo.name, - ext: pathInfo.ext.slice(1), - path: absPath, - data: FS.readFileSync( absPath, 'utf8' ), - css: null - }]; - return temp; - }); + var fmts = RECURSIVE_READ_DIR( tplFolder ).map( + function( absPath ) { + + var pathInfo = PATH.parse(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 outFmt = ''; + var portion = pathInfo.dir.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: [] }; + + var obj = { + path: absPath, + ext: pathInfo.ext.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; + } + ); // Add freebie formats every theme gets - fmts.push( [ 'json', { title: 'json', pre: 'json', ext: 'json', path: null, data: null } ] ); - fmts.push( [ 'yml', { title: 'yaml', pre: 'yml', ext: 'yml', path: null, data: null } ] ); + 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 }; // Now, get all the CSS files... - this.cssFiles = fmts.filter(function( fmt ){ return fmt[1].ext === 'css'; }); + this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; }); // ...and assemble information on them this.cssFiles.forEach(function( cssf ) { // For each CSS file, get its corresponding HTML file var idx = _.findIndex(fmts, function( fmt ) { - return fmt[1].pre === cssf[1].pre && fmt[1].ext === 'html' + return fmt.pre === cssf.pre && fmt.ext === 'html' }); - fmts[ idx ][1].css = cssf[1].data; - fmts[ idx ][1].cssPath = cssf[1].path; + fmts[ idx ].css = cssf.data; + fmts[ idx ].cssPath = cssf.path; }); // Remove CSS files from the formats array fmts = fmts.filter( function( fmt) { - return fmt[1].ext !== 'css'; + return fmt.ext !== 'css'; }); // Create a hash out of the formats for this theme - this.formats = _.object( fmts ); + this.formats = formatsHash; // Set the official theme name this.name = PATH.parse( themeFolder ).name; diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 8e61ad6..0c03a63 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -76,7 +76,7 @@ module.exports = function () { targets.push.apply(targets, fmat === '.all' ? Object.keys( theTheme.formats ).map(function(k){ var z = theTheme.formats[k]; - return { file: to.replace(/all$/g,z.pre), fmt: z } + return { file: to.replace(/all$/g,z.outFormat), fmt: z } }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); }); @@ -95,17 +95,34 @@ module.exports = function () { */ function single( fi, theme ) { try { - var f = fi.file, fType = fi.fmt.ext, fName = path.basename(f,'.'+fType); - var fObj = _.property( fi.fmt.pre )( theme.formats ); - var fOut = path.join( f.substring( 0, f.lastIndexOf('.')+1 ) + fObj.pre); + var f = fi.file, fType = fi.fmt.outFormat, fName = path.basename(f,'.'+fType); - _log( 'Generating '.useful + fi.fmt.title.toUpperCase().useful.bold + ' resume: '.useful + - path.relative(process.cwd(), f ).useful.bold ); + if( fi.fmt.files && fi.fmt.files.length ) { + fi.fmt.files.forEach( function( form ) { - var theFormat = _fmts.filter( - function( fmt ) { return fmt.name === fi.fmt.pre; })[0]; - MKDIRP.sync( path.dirname( fOut ) ); // Ensure dest folder exists; - theFormat.gen.generate( rez, fOut, _opts ); + if( form.ext === 'css' ) + return; + + _log( 'Generating '.useful + form.title.toUpperCase().useful.bold + ' resume: '.useful + + path.relative(process.cwd(), f ).useful.bold ); + + var theFormat = _fmts.filter( + function( fmt ) { return fmt.name === form.pre; })[0]; + + MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; + theFormat.gen.generate( rez, f, _opts ); + + }); + } + else { + _log( 'Generating '.useful + fi.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + + path.relative(process.cwd(), f ).useful.bold ); + + var theFormat = _fmts.filter( + function( fmt ) { return fmt.name === fi.fmt.outFormat; })[0]; + MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; + theFormat.gen.generate( rez, f, _opts ); + } } catch( ex ) { _err( ex ); diff --git a/src/gen/html-generator.js b/src/gen/html-generator.js index a7740ad..4c33009 100644 --- a/src/gen/html-generator.js +++ b/src/gen/html-generator.js @@ -25,8 +25,8 @@ HTML resume generator for FluentCV. , outFolder = PATH.parse( info.outputFile ).dir, that = this; info.theme.cssFiles.forEach( function( f ) { - var fi = PATH.parse( f[1].path ); - FS.copySync( f[1].path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) { + var fi = PATH.parse( f.path ); + FS.copySync( f.path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) { throw { fluenterror: that.codes.copyCss, data: [cssSrc,cssDst] }; }); }); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 3f5b0d7..c25c956 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -96,12 +96,16 @@ Template-based resume generator base for FluentCV. // Load theme and CSS data var tplFolder = PATH.join( tFolder, 'src' ); var curFmt = theme.getFormat( this.format ); - var cssInfo = { file: curFmt.css ? curFmt.cssPath : null, data: curFmt.css || null }; - // Compile and invoke the template! - var mk = this.single( rez, curFmt.data, this.format, cssInfo, this.opts ); - this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); - FS.writeFileSync( f, mk, { encoding: 'utf8', flags: 'w' } ); + var that = this; + curFmt.files.forEach(function(tplInfo){ + + var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null }; + // Compile and invoke the template! + var mk = that.single( rez, tplInfo.data, that.format, cssInfo, that.opts ); + that.onBeforeSave && (mk = that.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); + FS.writeFileSync( f, mk, { encoding: 'utf8', flags: 'w' } ); + }); }, From 8ee271624554d7a478a2f97c9abc851e46a54d0f Mon Sep 17 00:00:00 2001 From: devlinjd Date: Mon, 7 Dec 2015 10:16:38 -0500 Subject: [PATCH 13/29] Scrub theme.js. --- src/core/theme.js | 100 +++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/src/core/theme.js b/src/core/theme.js index 3fbcb01..6657c61 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -33,72 +33,74 @@ Abstract theme representation. return friendly[val] || val; } - // Open the theme.json file; should have the same name as folder + // Open the [theme-name].json file; should have the same name as folder this.folder = themeFolder; var pathInfo = PATH.parse( themeFolder ); var themeFile = PATH.join( themeFolder, pathInfo.base + '.json' ); var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) ); + + // Move properties from the theme JSON file to the theme object EXTEND( true, this, themeInfo ); + // Set up a hash of formats supported by this theme. var formatsHash = { }; - // Iterate over all files in the theme folder, producing an array, fmts, - // containing info for each file. + // Establish the base theme folder var tplFolder = PATH.join( themeFolder, 'src' ); - var fmts = RECURSIVE_READ_DIR( tplFolder ).map( - function( absPath ) { - var pathInfo = PATH.parse(absPath); + // 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 outFmt = ''; - var portion = pathInfo.dir.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: [] }; - - var obj = { - path: absPath, - ext: pathInfo.ext.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; + // 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 = PATH.parse(absPath); + var outFmt = ''; + var portion = pathInfo.dir.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: [] }; + + // Create the file representation object. + var obj = { + path: absPath, + ext: pathInfo.ext.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; + }); // 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 }; // Now, get all the CSS files... - this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; }); - - // ...and assemble information on them - this.cssFiles.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' + (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; @@ -109,7 +111,7 @@ Abstract theme representation. return fmt.ext !== 'css'; }); - // Create a hash out of the formats for this theme + // Cache the formats hash this.formats = formatsHash; // Set the official theme name From 5a716dff164da7d2b56f37a1ad264d1bff9a6186 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Mon, 7 Dec 2015 16:39:59 -0500 Subject: [PATCH 14/29] Add basic multiplexing support. --- src/core/theme.js | 114 +++++++++++++++++++++++--------- src/eng/underscore-generator.js | 28 +++++--- src/fluentcmd.js | 16 ++--- src/gen/latex-generator.js | 2 +- 4 files changed, 112 insertions(+), 48 deletions(-) diff --git a/src/core/theme.js b/src/core/theme.js index 6657c61..57565b3 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -27,26 +27,62 @@ Abstract theme representation. */ Theme.prototype.open = function( themeFolder ) { - function friendlyName( val ) { - val = val.trim().toLowerCase(); - var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }; - return friendly[val] || val; - } - // Open the [theme-name].json file; should have the same name as folder this.folder = themeFolder; var pathInfo = PATH.parse( themeFolder ); var themeFile = PATH.join( themeFolder, pathInfo.base + '.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 ); + } + 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 = PATH.parse( 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 = { }; // Establish the base theme folder - var tplFolder = PATH.join( themeFolder, 'src' ); + 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 @@ -73,7 +109,8 @@ Abstract theme representation. } // We should have a valid output format now. - formatsHash[ outFmt ] = formatsHash[outFmt] || { outFormat: outFmt, files: [] }; + formatsHash[ outFmt ] = + formatsHash[outFmt] || { outFormat: outFmt, files: [] }; // Create the file representation object. var obj = { @@ -91,10 +128,6 @@ Abstract theme representation. return obj; }); - // 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 }; - // Now, get all the CSS files... (this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; })) .forEach(function( cssf ) { @@ -111,28 +144,49 @@ Abstract theme representation. return fmt.ext !== 'css'; }); - // Cache the formats hash - this.formats = formatsHash; + return formatsHash; + } - // Set the official theme name - this.name = PATH.parse( themeFolder ).name; + function loadExplicit() { - return this; - }; + var formatsHash = { }; + var that = this; - /** - Determine if the theme supports the specified output format. - */ - Theme.prototype.hasFormat = function( fmt ) { - return _.has( this.formats, fmt ); - }; + // Establish the base theme folder + var tplFolder = this.folder;//PATH.join( this.folder, 'src' ); - /** - Determine if the theme supports the specified output format. - */ - Theme.prototype.getFormat = function( fmt ) { - return this.formats[ fmt ]; - }; + // Iterate over all keys in the "formats" section of the theme JSON file. + // Each key will be a format (html, latex, pdf, etc) with some data. + Object.keys( this.formats ).forEach( function( k ) { + + formatsHash[ k ] = { + outFormat: k, + files: that.formats[ k ].files.map(function(fi){ + + var absPath = PATH.join( tplFolder, fi ); + var pathInfo = PATH.parse( absPath ); + + + return { + path: absPath, + ext: pathInfo.ext.slice(1), + title: friendlyName( k ), + pre: k, + outFormat: k, + data: FS.readFileSync( absPath, 'utf8' ), + css: null + }; + }) + }; + }); + 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/underscore-generator.js b/src/eng/underscore-generator.js index f744153..a8102db 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -10,27 +10,37 @@ Underscore template generate for FluentCV. module.exports = function( json, jst, format, cssInfo, opts ) { // Tweak underscore's default template delimeters - _.templateSettings = opts.template; + var delims = opts.themeObj.delimeters || opts.template; + if( opts.themeObj.delimeters ) { + delims = _.mapObject( delims, function(val,key) { + return new RegExp( val, "ig") + }); + } + _.templateSettings = delims; // Convert {{ someVar }} to {% print(filt.out(someVar) %} // Convert {{ someVar|someFilter }} to {% print(filt.someFilter(someVar) %} - jst = jst.replace( _.templateSettings.interpolate, function replace(m, p1) { + jst = jst.replace( delims.interpolate, function replace(m, p1) { if( p1.indexOf('|') > -1 ) { var terms = p1.split('|'); - return '{% print( filt.' + terms[1] + '( ' + terms[0] + ' )) %}'; + return '[~ print( filt.' + terms[1] + '( ' + terms[0] + ' )) ]]'; } else { - return '{% print( filt.out(' + p1 + ') ) %}'; + return '[~ print( filt.out(' + p1 + ') ) ]]'; } }); // Strip {# comments #} - jst = jst.replace( _.templateSettings.comment, ''); - + jst = jst.replace( delims.comment, ''); // Compile and run the template. TODO: avoid unnecessary recompiles. - jst = _.template(jst)({ r: json, filt: opts.filters, cssInfo: cssInfo, headFragment: opts.headFragment || '' }); - - return jst; + var compiled = _.template(jst); + var ret = compiled({ + r: json, + filt: opts.filters, + cssInfo: cssInfo, + headFragment: opts.headFragment || '' + }); + return ret; }; diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 0c03a63..5063911 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -93,21 +93,21 @@ module.exports = function () { @param f Full path to the destination resume to generate, for example, "/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt". */ - function single( fi, theme ) { + function single( targetInfo, theme ) { try { - var f = fi.file, fType = fi.fmt.outFormat, fName = path.basename(f,'.'+fType); + var f = targetInfo.file, fType = targetInfo.fmt.outFormat, fName = path.basename(f,'.'+fType); - if( fi.fmt.files && fi.fmt.files.length ) { - fi.fmt.files.forEach( function( form ) { + if( targetInfo.fmt.files && targetInfo.fmt.files.length ) { + targetInfo.fmt.files.forEach( function( form ) { if( form.ext === 'css' ) return; - _log( 'Generating '.useful + form.title.toUpperCase().useful.bold + ' resume: '.useful + + _log( 'Generating '.useful + targetInfo.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + path.relative(process.cwd(), f ).useful.bold ); var theFormat = _fmts.filter( - function( fmt ) { return fmt.name === form.pre; })[0]; + function( fmt ) { return fmt.name === targetInfo.fmt.outFormat; })[0]; MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; theFormat.gen.generate( rez, f, _opts ); @@ -115,11 +115,11 @@ module.exports = function () { }); } else { - _log( 'Generating '.useful + fi.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + + _log( 'Generating '.useful + targetInfo.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + path.relative(process.cwd(), f ).useful.bold ); var theFormat = _fmts.filter( - function( fmt ) { return fmt.name === fi.fmt.outFormat; })[0]; + function( fmt ) { return fmt.name === targetInfo.fmt.outFormat; })[0]; MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; theFormat.gen.generate( rez, f, _opts ); } diff --git a/src/gen/latex-generator.js b/src/gen/latex-generator.js index 43b6b67..1e32315 100644 --- a/src/gen/latex-generator.js +++ b/src/gen/latex-generator.js @@ -11,7 +11,7 @@ LaTeXGenerator generates a LaTeX resume via TemplateGenerator. var LaTeXGenerator = module.exports = TemplateGenerator.extend({ init: function(){ - this._super( 'tex', 'tex' ); + this._super( 'latex', 'tex' ); } }); From fcaeb381fe1796d3caaa52cad90e2600a4a456bd Mon Sep 17 00:00:00 2001 From: devlinjd Date: Mon, 7 Dec 2015 21:24:14 -0500 Subject: [PATCH 15/29] Gather. --- src/core/fresh-resume.js | 21 ++ src/core/theme.js | 102 ++++-- src/eng/underscore-generator.js | 5 +- src/fluentcmd.js | 601 +++++++++++++++++--------------- src/gen/template-generator.js | 27 +- 5 files changed, 438 insertions(+), 318 deletions(-) diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index fb647ef..b2fb8d3 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -179,6 +179,27 @@ Definition of the FRESHResume class. }); }; + /** + Return the specified network profile. + */ + FreshResume.prototype.getProfile = function( socialNetwork ) { + socialNetwork = socialNetwork.trim().toLowerCase(); + return this.social && _.find( this.social, function(sn) { + return 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 ) { + socialNetwork = socialNetwork.trim().toLowerCase(); + return this.social && _.filter( this.social, function(sn){ + return sn.network.trim().toLowerCase() === socialNetwork + }); + } + /** Determine if the sheet includes a specific skill. */ diff --git a/src/core/theme.js b/src/core/theme.js index 57565b3..ed79ea4 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -149,36 +149,98 @@ Abstract theme representation. function loadExplicit() { - var formatsHash = { }; var that = this; + // Set up a hash of formats supported by this theme. + var formatsHash = { }; // Establish the base theme folder - var tplFolder = this.folder;//PATH.join( this.folder, 'src' ); + var tplFolder = PATH.join( this.folder, 'src' ); - // Iterate over all keys in the "formats" section of the theme JSON file. - // Each key will be a format (html, latex, pdf, etc) with some data. - Object.keys( this.formats ).forEach( function( k ) { + var act = null; - formatsHash[ k ] = { - outFormat: k, - files: that.formats[ k ].files.map(function(fi){ + // 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 ) { - var absPath = PATH.join( tplFolder, fi ); - var pathInfo = PATH.parse( absPath ); + act = null; + // If this file is mentioned in the theme's JSON file under "transforms" + var pathInfo = PATH.parse(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.dir.replace(tplFolder,''); + if( portion && portion.trim() ) { + var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig; + var res = reg.exec( portion ); + res && (outFmt = res[1]); + } + } - return { - path: absPath, - ext: pathInfo.ext.slice(1), - title: friendlyName( k ), - pre: k, - outFormat: k, - data: FS.readFileSync( absPath, 'utf8' ), - css: 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 ) + } + + // We should have a valid output format now. + formatsHash[ outFmt ] = + formatsHash[outFmt] || { outFormat: outFmt, files: [] }; + + // Create the file representation object. + var obj = { + action: act, + orgPath: PATH.relative(that.folder, absPath), + path: absPath, + ext: pathInfo.ext.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'; + }); + + // Object.keys( formatsHash ).forEach(function(k){ + // formatsHash[ k ].files.forEach(function(xhs){ + // console.log(xhs.orgPath); + // }); + // }); + return formatsHash; } diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js index a8102db..d25089d 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -23,10 +23,10 @@ Underscore template generate for FluentCV. jst = jst.replace( delims.interpolate, function replace(m, p1) { if( p1.indexOf('|') > -1 ) { var terms = p1.split('|'); - return '[~ print( filt.' + terms[1] + '( ' + terms[0] + ' )) ]]'; + return '[~ print( filt.' + terms[1] + '( ' + terms[0] + ' )) ~]'; } else { - return '[~ print( filt.out(' + p1 + ') ) ]]'; + return '[~ print( filt.out(' + p1 + ') ) ~]'; } }); @@ -34,6 +34,7 @@ Underscore template generate for FluentCV. jst = jst.replace( delims.comment, ''); // Compile and run the template. TODO: avoid unnecessary recompiles. var compiled = _.template(jst); + var ret = compiled({ r: json, filt: opts.filters, diff --git a/src/fluentcmd.js b/src/fluentcmd.js index 5063911..cb8b6c9 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -4,318 +4,343 @@ Internal resume generation logic for FluentCV. @module fluentcmd.js */ -module.exports = function () { +(function() { + module.exports = function () { - // We don't mind pseudo-globals here - var path = require( 'path' ) - , extend = require( './utils/extend' ) - , unused = require('./utils/string') - , FS = require('fs') - , _ = require('underscore') - , FLUENT = require('./fluentlib') - , PATH = require('path') - , MKDIRP = require('mkdirp') - //, COLORS = require('colors') - , rez, _log, _err; + var path = require( 'path' ) + , extend = require( './utils/extend' ) + , unused = require('./utils/string') + , FS = require('fs') + , _ = require('underscore') + , FLUENT = require('./fluentlib') + , PATH = require('path') + , MKDIRP = require('mkdirp') + //, COLORS = require('colors') + , rez, _log, _err; - /** - Given a source JSON resume, a destination resume path, and a theme file, - generate 0..N resumes in the desired formats. - @param src Path to the source JSON resume file: "rez/resume.json". - @param dst An array of paths to the target resume file(s). - @param theme Friendly name of the resume theme. Defaults to "modern". - @param logger Optional logging override. - */ - function generate( src, dst, opts, logger, errHandler ) { + /** + Given a source JSON resume, a destination resume path, and a theme file, + generate 0..N resumes in the desired formats. + @param src Path to the source JSON resume file: "rez/resume.json". + @param dst An array of paths to the target resume file(s). + @param theme Friendly name of the resume theme. Defaults to "modern". + @param logger Optional logging override. + */ + function generate( src, dst, opts, logger, errHandler ) { - _log = logger || console.log; - _err = errHandler || error; + _log = logger || console.log; + _err = errHandler || error; - //_opts = extend( true, _opts, opts ); - _opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern'; - _opts.prettify = opts.prettify === true ? _opts.prettify : false; + //_opts = extend( true, _opts, opts ); + _opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern'; + _opts.prettify = opts.prettify === true ? _opts.prettify : false; - // Load input resumes... - if(!src || !src.length) { throw { fluenterror: 3 }; } - var sheets = loadSourceResumes( src ); + // Load input resumes... + if(!src || !src.length) { throw { fluenterror: 3 }; } + var sheets = loadSourceResumes( src ); - // Merge input resumes... - var msg = ''; - rez = _.reduceRight( sheets, function( a, b, idx ) { - msg += ((idx == sheets.length - 2) ? 'Merging '.gray + a.imp.fileName : '') - + ' onto '.gray + b.imp.fileName; - return extend( true, b, a ); - }); - msg && _log(msg); + // Merge input resumes... + var msg = ''; + rez = _.reduceRight( sheets, function( a, b, idx ) { + msg += ((idx == sheets.length - 2) ? + 'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName; + return extend( true, b, a ); + }); + msg && _log(msg); - // Verify the specified theme name/path - var relativeThemeFolder = '../node_modules/fluent-themes/themes'; - var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme ); - var exists = require('./utils/file-exists'); - if (!exists( tFolder )) { - tFolder = PATH.resolve( _opts.theme ); + // Verify the specified theme name/path + var relativeThemeFolder = '../node_modules/fluent-themes/themes'; + var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme); + var exists = require('./utils/file-exists'); if (!exists( tFolder )) { - throw { fluenterror: 1, data: _opts.theme }; - } - } - - // Load the theme - var theTheme = new FLUENT.Theme().open( tFolder ); - _opts.themeObj = theTheme; - _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + (' theme (' + - Object.keys(theTheme.formats).length + ' formats)').info ); - - // Expand output resumes... (can't use map() here) - var targets = [], that = this; - ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { - - var to = path.resolve(t), - pa = path.parse(to), - fmat = pa.ext || '.all'; - - targets.push.apply(targets, fmat === '.all' ? - Object.keys( theTheme.formats ).map(function(k){ - var z = theTheme.formats[k]; - return { file: to.replace(/all$/g,z.outFormat), fmt: z } - }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); - - }); - - // Run the transformation! - var finished = targets.map( function(t) { return single(t, theTheme); } ); - - // Don't send the client back empty-handed - return { sheet: rez, targets: targets, processed: finished }; - } - - /** - Generate a single resume of a specific format. - @param f Full path to the destination resume to generate, for example, - "/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt". - */ - function single( targetInfo, theme ) { - try { - var f = targetInfo.file, fType = targetInfo.fmt.outFormat, fName = path.basename(f,'.'+fType); - - if( targetInfo.fmt.files && targetInfo.fmt.files.length ) { - targetInfo.fmt.files.forEach( function( form ) { - - if( form.ext === 'css' ) - return; - - _log( 'Generating '.useful + targetInfo.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + - path.relative(process.cwd(), f ).useful.bold ); - - var theFormat = _fmts.filter( - function( fmt ) { return fmt.name === targetInfo.fmt.outFormat; })[0]; - - MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; - theFormat.gen.generate( rez, f, _opts ); - - }); - } - else { - _log( 'Generating '.useful + targetInfo.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + - path.relative(process.cwd(), f ).useful.bold ); - - var theFormat = _fmts.filter( - function( fmt ) { return fmt.name === targetInfo.fmt.outFormat; })[0]; - MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; - theFormat.gen.generate( rez, f, _opts ); - } - } - catch( ex ) { - _err( ex ); - } - } - - /** - Handle an exception. - */ - function error( ex ) { - throw ex; - } - - /** - Validate 1 to N resumes in either FRESH or JSON Resume format. - */ - function validate( src, unused, opts, logger ) { - _log = logger || console.log; - if( !src || !src.length ) { throw { fluenterror: 6 }; } - var isValid = true; - - var validator = require('is-my-json-valid'); - var schemas = { - fresh: require('FRESCA'), - jars: require('./core/resume.json') - }; - - // Load input resumes... - var sheets = loadSourceResumes(src, function( res ) { - try { - return { - file: res, - raw: FS.readFileSync( res, 'utf8' ) - }; - } - catch( ex ) { - throw ex; - } - }); - - sheets.forEach( function( rep ) { - - try { - var rez = JSON.parse( rep.raw ); - } - catch( ex ) { - _log('Validating '.info + rep.file.infoBold + ' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold); - - if (ex instanceof SyntaxError) { - // Invalid JSON - _log( '--> '.bold.red + rep.file.toUpperCase().red + ' contains invalid JSON. Unable to validate.'.red ); - _log( (' INTERNAL: ' + ex).red ); + tFolder = PATH.resolve( _opts.theme ); + if (!exists( tFolder )) { + throw { fluenterror: 1, data: _opts.theme }; } - else { - - _log(('ERROR: ' + ex.toString()).red.bold); - } - return; } - var isValid = false; - var style = 'useful'; - var errors = []; + // Load the theme + var theTheme = new FLUENT.Theme().open( tFolder ); + _opts.themeObj = theTheme; + _log( 'Applying '.info + theTheme.name.toUpperCase().infoBold + + (' theme (' +Object.keys(theTheme.formats).length + ' formats)').info); - try { + // Expand output resumes... (can't use map() here) + var targets = [], that = this; + ( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) { + var to = path.resolve(t), + pa = path.parse(to), + fmat = pa.ext || '.all'; + targets.push.apply(targets, fmat === '.all' ? + Object.keys( theTheme.formats ).map(function(k){ + var z = theTheme.formats[k]; + return { file: to.replace(/all$/g,z.outFormat), fmt: z } + }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); - var fmt = rez.meta && rez.meta.format === 'FRESH@0.1.0' ? 'fresh':'jars'; - var validate = validator( schemas[ fmt ], { // Note [1] - formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } - }); - - isValid = validate( rez ); - if( !isValid ) { - style = 'warn'; - errors = validate.errors; - } - - } - catch(ex) { - - } - - _log( 'Validating '.info + rep.file.infoBold + ' against '.info + - fmt.replace('jars','JSON Resume').toUpperCase().infoBold + ' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold ); - - errors.forEach(function(err,idx){ - _log( '--> '.bold.yellow + ( err.field.replace('data.','resume.').toUpperCase() - + ' ' + err.message).yellow ); }); + // Run the transformation! + var finished = targets.map( function(t) { return single(t, theTheme); }); - - }); - } - - /** - Convert between FRESH and JRS formats. - */ - function convert( src, dst, opts, logger ) { - _log = logger || console.log; - if( !src || !src.length ) { throw { fluenterror: 6 }; } - if( !dst || !dst.length ) { - if( src.length === 1 ) { throw { fluenterror: 5 }; } - else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; } - else { throw { fluenterror: 5 }; } + // Don't send the client back empty-handed + return { sheet: rez, targets: targets, processed: finished }; } - if( src && dst && src.length && dst.length && src.length !== dst.length ) { - throw { fluenterror: 7 }; + + /** + Generate a single resume of a specific format. + @param f Full path to the destination resume to generate, for example, + "/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt". + */ + function single( targInfo, theme ) { + try { + var f = targInfo.file + , fType = targInfo.fmt.outFormat + , fName = path.basename(f, '.' + fType); + + // If targInfo.fmt.files exists, this theme has an explicit "files" + // section in its theme.json file. + if( targInfo.fmt.files && targInfo.fmt.files.length ) { + + _log( 'Generating '.useful + + targInfo.fmt.outFormat.toUpperCase().useful.bold + + ' resume: '.useful + path.relative(process.cwd(), f ).useful.bold); + + var theFormat = _fmts.filter( + function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; + MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; + theFormat.gen.generate( rez, f, _opts ); + + // targInfo.fmt.files.forEach( function( form ) { + // + // if( form.action === 'transform' ) { + // var theFormat = _fmts.filter( function( fmt ) { + // return fmt.name === targInfo.fmt.outFormat; + // })[0]; + // MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; + // theFormat.gen.generate( rez, f, _opts ); + // } + // else if( form.action === null ) { + // // Copy the file + // } + // + // }); + + } + // Otherwise the theme has no files section + else { + _log( 'Generating '.useful + + targInfo.fmt.outFormat.toUpperCase().useful.bold + + ' resume: '.useful + path.relative(process.cwd(), f ).useful.bold); + + var theFormat = _fmts.filter( + function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; + MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; + theFormat.gen.generate( rez, f, _opts ); + } + } + catch( ex ) { + _err( ex ); + } } - var sheets = loadSourceResumes( src ); - sheets.forEach(function(sheet, idx){ - var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; - var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; - _log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' + sourceFormat + ') to ').useful + dst[0].useful.bold + - (' (' + targetFormat + ').').useful ); - sheet.saveAs( dst[idx], targetFormat ); - }); - } - /** - Create a new empty resume in either FRESH or JRS format. - */ - function create( src, dst, opts, logger ) { - _log = logger || console.log; - dst = src || ['resume.json']; - dst.forEach( function( t ) { - var safeFormat = opts.format.toUpperCase(); - _log('Creating new '.useful +safeFormat.useful.bold+ ' resume: '.useful + t.useful.bold); - MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists; - FLUENT[ safeFormat + 'Resume' ].default().save( t ); - }); - } - - /** - Display help documentation. - */ - function help() { - console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).useful.bold ); - } - - function loadSourceResumes( src, fn ) { - return src.map( function( res ) { - _log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info + res.cyan.bold ); - return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res ); - }); - } - - /** - Supported resume formats. - */ - var _fmts = [ - { 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() } - ]; - - /** - Default FluentCV options. - */ - var _opts = { - 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 + /** + Handle an exception. + */ + function error( ex ) { + throw ex; } - }; - /** - Internal module interface. Used by FCV Desktop and HMR. - */ - return { - verbs: { - build: generate, - validate: validate, - convert: convert, - new: create, - help: help - }, - lib: require('./fluentlib'), - options: _opts, - formats: _fmts - }; + /** + Validate 1 to N resumes in either FRESH or JSON Resume format. + */ + function validate( src, unused, opts, logger ) { + _log = logger || console.log; + if( !src || !src.length ) { throw { fluenterror: 6 }; } + var isValid = true; -}(); + var validator = require('is-my-json-valid'); + var schemas = { + fresh: require('FRESCA'), + jars: require('./core/resume.json') + }; + + // Load input resumes... + var sheets = loadSourceResumes(src, function( res ) { + try { + return { + file: res, + raw: FS.readFileSync( res, 'utf8' ) + }; + } + catch( ex ) { + throw ex; + } + }); + + sheets.forEach( function( rep ) { + + try { + var rez = JSON.parse( rep.raw ); + } + catch( ex ) { + _log('Validating '.info + rep.file.infoBold + + ' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold); + + if (ex instanceof SyntaxError) { + // Invalid JSON + _log( '--> '.bold.red + rep.file.toUpperCase().red + + ' contains invalid JSON. Unable to validate.'.red ); + _log( (' INTERNAL: ' + ex).red ); + } + else { + + _log(('ERROR: ' + ex.toString()).red.bold); + } + return; + } + + var isValid = false; + var style = 'useful'; + var errors = []; + + try { + + var fmt = rez.meta && + (rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars'; + var validate = validator( schemas[ fmt ], { // Note [1] + formats: { + date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ + } + }); + + isValid = validate( rez ); + if( !isValid ) { + style = 'warn'; + errors = validate.errors; + } + + } + catch(ex) { + + } + + _log( 'Validating '.info + rep.file.infoBold + ' against '.info + + fmt.replace('jars','JSON Resume').toUpperCase().infoBold + + ' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold ); + + errors.forEach(function(err,idx) { + _log( '--> '.bold.yellow + + (err.field.replace('data.','resume.').toUpperCase() + ' ' + + err.message).yellow ); + }); + + }); + } + + /** + Convert between FRESH and JRS formats. + */ + function convert( src, dst, opts, logger ) { + _log = logger || console.log; + if( !src || !src.length ) { throw { fluenterror: 6 }; } + if( !dst || !dst.length ) { + if( src.length === 1 ) { throw { fluenterror: 5 }; } + else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; } + else { throw { fluenterror: 5 }; } + } + if( src && dst && src.length && dst.length && src.length !== dst.length ) { + throw { fluenterror: 7 }; + } + var sheets = loadSourceResumes( src ); + sheets.forEach(function(sheet, idx){ + var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH'; + var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS'; + _log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' + + sourceFormat + ') to ').useful + dst[0].useful.bold + + (' (' + targetFormat + ').').useful ); + sheet.saveAs( dst[idx], targetFormat ); + }); + } + + /** + Create a new empty resume in either FRESH or JRS format. + */ + function create( src, dst, opts, logger ) { + _log = logger || console.log; + dst = src || ['resume.json']; + dst.forEach( function( t ) { + var safeFormat = opts.format.toUpperCase(); + _log('Creating new '.useful +safeFormat.useful.bold+ ' resume: '.useful + + t.useful.bold); + MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists; + FLUENT[ safeFormat + 'Resume' ].default().save( t ); + }); + } + + /** + Display help documentation. + */ + function help() { + console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ) + .useful.bold ); + } + + function loadSourceResumes( src, fn ) { + return src.map( function( res ) { + _log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info + + res.cyan.bold ); + return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res ); + }); + } + + /** + Supported resume formats. + */ + var _fmts = [ + { 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() } + ]; + + /** + Default FluentCV options. + */ + var _opts = { + 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 + } + }; + + /** + Internal module interface. Used by FCV Desktop and HMR. + */ + return { + verbs: { + build: generate, + validate: validate, + convert: convert, + new: create, + help: help + }, + lib: require('./fluentlib'), + options: _opts, + formats: _fmts + }; + + }(); + +}()); // [1]: JSON.parse throws SyntaxError on invalid JSON. See: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index c25c956..6ab7016 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -10,6 +10,7 @@ Template-based resume generator base for FluentCV. , MD = require( 'marked' ) , XML = require( 'xml-escape' ) , PATH = require('path') + , MKDIRP = require('mkdirp') , BaseGenerator = require( './base-generator' ) , EXTEND = require('../utils/extend') , Theme = require('../core/theme'); @@ -90,6 +91,8 @@ Template-based resume generator base for FluentCV. } } + var outFolder = PATH.parse(f).dir; + // Load the theme var theme = opts.themeObj || new Theme().open( tFolder ); @@ -99,20 +102,28 @@ Template-based resume generator base for FluentCV. var that = this; curFmt.files.forEach(function(tplInfo){ + if( tplInfo.action === 'transform' ) { + var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null }; + var mk = that.single( rez, tplInfo.data, that.format, cssInfo, that.opts ); + that.onBeforeSave && (mk = that.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); - var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null }; - // Compile and invoke the template! - var mk = that.single( rez, tplInfo.data, that.format, cssInfo, that.opts ); - that.onBeforeSave && (mk = that.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); - FS.writeFileSync( f, mk, { encoding: 'utf8', flags: 'w' } ); + var thisFilePath = PATH.join(outFolder, tplInfo.orgPath); + MKDIRP.sync( PATH.dirname(thisFilePath) ); + console.log('Would save to ' + thisFilePath); + + FS.writeFileSync( thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); + } }); }, /** - Perform a single resume JSON-to-DEST resume transformation. Exists as a - separate function in order to expose string-based transformations to clients - who don't have access to filesystem resources (in-browser, etc.). + 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 ) { From 7c58f0ea965f2969c3fcc2f9d8e42081081948b8 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Tue, 8 Dec 2015 10:13:04 -0500 Subject: [PATCH 16/29] Add symlink support. --- src/core/theme.js | 15 +++++------ src/gen/template-generator.js | 50 ++++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/core/theme.js b/src/core/theme.js index ed79ea4..7752774 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -80,6 +80,7 @@ Abstract theme representation. // Set up a hash of formats supported by this theme. var formatsHash = { }; + var that = this; // Establish the base theme folder var tplFolder = PATH.join( this.folder, 'src' ); @@ -110,7 +111,7 @@ Abstract theme representation. // We should have a valid output format now. formatsHash[ outFmt ] = - formatsHash[outFmt] || { outFormat: outFmt, files: [] }; + formatsHash[outFmt] || { outFormat: outFmt, files: [], symLinks: that.formats[ outFmt ].symLinks }; // Create the file representation object. var obj = { @@ -199,7 +200,11 @@ Abstract theme representation. // We should have a valid output format now. formatsHash[ outFmt ] = - formatsHash[outFmt] || { outFormat: outFmt, files: [] }; + formatsHash[ outFmt ] || { + outFormat: outFmt, + files: [], + symLinks: that.formats[ outFmt ].symLinks + }; // Create the file representation object. var obj = { @@ -235,12 +240,6 @@ Abstract theme representation. return fmt.ext !== 'css'; }); - // Object.keys( formatsHash ).forEach(function(k){ - // formatsHash[ k ].files.forEach(function(xhs){ - // console.log(xhs.orgPath); - // }); - // }); - return formatsHash; } diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 6ab7016..f11b80e 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -5,7 +5,7 @@ Template-based resume generator base for FluentCV. (function() { - var FS = require( 'fs' ) + var FS = require( 'fs-extra' ) , _ = require( 'underscore' ) , MD = require( 'marked' ) , XML = require( 'xml-escape' ) @@ -19,7 +19,7 @@ Template-based resume generator base for FluentCV. var _defaultOpts = { engine: 'underscore', keepBreaks: true, - freezeBreaks: true, + freezeBreaks: false, nSym: '&newl;', // newline entity rSym: '&retn;', // return entity template: { @@ -87,7 +87,7 @@ Template-based resume generator base for FluentCV. if (!exists( tFolder )) { tFolder = PATH.resolve( this.opts.theme ); if (!exists( tFolder )) { - throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme }; + throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme}; } } @@ -102,19 +102,43 @@ Template-based resume generator base for FluentCV. var that = this; curFmt.files.forEach(function(tplInfo){ - if( tplInfo.action === 'transform' ) { - var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null }; - var mk = that.single( rez, tplInfo.data, that.format, cssInfo, that.opts ); - that.onBeforeSave && (mk = that.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); - - var thisFilePath = PATH.join(outFolder, tplInfo.orgPath); - MKDIRP.sync( PATH.dirname(thisFilePath) ); - console.log('Would save to ' + thisFilePath); - - FS.writeFileSync( thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); + if( tplInfo.action === 'transform' || tplInfo.action === null ) { + if( tplInfo.action === 'transform' ) { + var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null }; + var mk = that.single( rez, tplInfo.data, that.format, cssInfo, that.opts ); + that.onBeforeSave && (mk = that.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); + var thisFilePath = PATH.join(outFolder, tplInfo.orgPath); + try { + MKDIRP.sync( PATH.dirname(thisFilePath) ); + FS.writeFileSync( thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); + } + catch(ex) { + console.log(ex); + } + } + else if( tplInfo.action === null ) { + var thisFilePath = PATH.join(outFolder, tplInfo.orgPath); + try { + MKDIRP.sync( PATH.dirname(thisFilePath) ); + FS.copySync( tplInfo.path, thisFilePath ); + } + catch(ex) { + console.log(ex); + } + } } }); + // Create symlinks + 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]); + var type = PATH.parse( absLoc ).ext ? 'file' : 'junction'; // 'file', 'dir', or 'junction' (Windows only) + FS.symlinkSync( absTarg, absLoc, type); + }); + } + }, /** From 1a757e8a876bdf29317555b54ef1dc2a36c573a5 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Tue, 8 Dec 2015 21:12:19 -0500 Subject: [PATCH 17/29] Bump FRESCA version to 0.2.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee3a1b8..7932fe8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "homepage": "https://github.com/fluentdesk/fluentcv", "dependencies": { - "FRESCA": "fluentdesk/FRESCA#v0.1.0", + "fresca": "^0.2.0", "colors": "^1.1.2", "fluent-themes": "0.6.0-beta", "fs-extra": "^0.24.0", From 87c03b437c65afb95f8c33eb6c34e751499944fd Mon Sep 17 00:00:00 2001 From: devlinjd Date: Tue, 8 Dec 2015 22:21:42 -0500 Subject: [PATCH 18/29] Generate safe date times; don't hard-code. --- src/core/fresh-resume.js | 58 +++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index b2fb8d3..821ad6e 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -287,35 +287,39 @@ Definition of the FRESHResume class. function _parseDates() { var _fmt = require('./fluent-date').fmt; + var that = this; - this.employment.history && this.employment.history.forEach( function(job) { - job.safe = { - start: _fmt( job.start ), - end: _fmt( job.end || 'current' ) - }; - }); - this.education.history && this.education.history.forEach( function(edu) { - edu.safe = { - start: _fmt( edu.start ), - end: _fmt( edu.end || 'current' ) - }; - }); - this.service.history && this.service.history.forEach( function(vol) { - vol.safe = { - start: _fmt( vol.start ), - end: _fmt( vol.end || 'current' ) - }; - }); - this.recognition && this.recognition.forEach( function(rec) { - rec.safe = { - date: _fmt( rec.date ) - }; - }); - this.writing && this.writing.forEach( function(pub) { - pub.safe = { - date: _fmt( pub.date ) - }; + // TODO: refactor recursion + function replaceDatesInObject( obj ) { + + if( !obj ) return; + if( Object.prototype.toString.call( obj ) === '[object Array]' ) { + obj.forEach(function(elem){ + replaceDatesInObject( elem ); + }); + } + 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] && (!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( this ).forEach(function(member){ + replaceDatesInObject( that[ member ] ); }); + } /** From e8704e1374d580273681309b6be0d393140d809e Mon Sep 17 00:00:00 2001 From: devlinjd Date: Tue, 8 Dec 2015 22:22:14 -0500 Subject: [PATCH 19/29] Fix file generation glitch. --- src/core/theme.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/theme.js b/src/core/theme.js index 7752774..17a9dc5 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -110,12 +110,16 @@ Abstract theme representation. } // We should have a valid output format now. - formatsHash[ outFmt ] = - formatsHash[outFmt] || { outFormat: outFmt, files: [], symLinks: that.formats[ outFmt ].symLinks }; + formatsHash[ outFmt ] = formatsHash[outFmt] || { + outFormat: outFmt, + files: [] + }; // Create the file representation object. var obj = { + action: 'transform', path: absPath, + orgPath: PATH.relative(that.folder, absPath), ext: pathInfo.ext.slice(1), title: friendlyName( outFmt ), pre: outFmt, From f3c9f922636e60353404f5ccec08654ab9565f02 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Tue, 8 Dec 2015 22:22:33 -0500 Subject: [PATCH 20/29] Add baseline Markdownification. --- src/core/fresh-resume.js | 36 +++++++++++++++++++++++++++++++++ src/eng/underscore-generator.js | 16 +++------------ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index 821ad6e..6446f4a 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -11,6 +11,7 @@ Definition of the FRESHResume class. , _ = require('underscore') , PATH = require('path') , moment = require('moment') + , MD = require('marked') , CONVERTER = require('./convert'); /** @@ -70,6 +71,41 @@ Definition of the FRESHResume class. return JSON.stringify( obj, replacer, 2 ); }, + /** + + */ + FreshResume.prototype.markdownify = function() { + + var that = this; + var ret = extend(true, { }, this); + + // TODO: refactor recursion + function markdownifyStringsInObject( obj ) { + + if( !obj ) return; + if( Object.prototype.toString.call( obj ) === '[object Array]' ) { + obj.forEach(function(elem){ + markdownifyStringsInObject( elem ); + }); + } + else if (typeof obj === 'object') { + if( obj._isAMomentObject || obj.safe ) + return; + Object.keys( obj ).forEach(function(key) { + var sub = obj[key]; + if( typeof sub === 'string' || sub instanceof String ) + obj[key] = MD( obj[key] ); + }); + } + } + + Object.keys( ret ).forEach(function(member){ + markdownifyStringsInObject( that[ member ] ); + }); + + return ret; + }; + /** Convert this object to a JSON string, sanitizing meta-properties along the way. Don't override .toString(). diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js index d25089d..a560dd6 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -18,25 +18,15 @@ Underscore template generate for FluentCV. } _.templateSettings = delims; - // Convert {{ someVar }} to {% print(filt.out(someVar) %} - // Convert {{ someVar|someFilter }} to {% print(filt.someFilter(someVar) %} - jst = jst.replace( delims.interpolate, function replace(m, p1) { - if( p1.indexOf('|') > -1 ) { - var terms = p1.split('|'); - return '[~ print( filt.' + terms[1] + '( ' + terms[0] + ' )) ~]'; - } - else { - return '[~ print( filt.out(' + p1 + ') ) ~]'; - } - }); - // Strip {# comments #} jst = jst.replace( delims.comment, ''); // Compile and run the template. TODO: avoid unnecessary recompiles. var compiled = _.template(jst); + var mr = json.markdownify(); + var ret = compiled({ - r: json, + r: json.markdownify(), filt: opts.filters, cssInfo: cssInfo, headFragment: opts.headFragment || '' From 857de6575007f8dc6ad91e615c16919a8fd506c2 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 9 Dec 2015 00:13:58 -0500 Subject: [PATCH 21/29] More MEGADESK. --- src/core/fresh-resume.js | 39 ++++++++++++++++++++++++--------- src/core/theme.js | 7 +++--- src/eng/underscore-generator.js | 5 ++--- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index 6446f4a..afeaf42 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -57,6 +57,12 @@ Definition of the FRESHResume class. 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. @@ -77,30 +83,42 @@ Definition of the FRESHResume class. FreshResume.prototype.markdownify = function() { var that = this; - var ret = extend(true, { }, this); + var ret = this.dupe(); + + function MDIN(txt){ + return MD(txt || '' ).replace(/^\s*\|\<\/p\>\s*$/gi, ''); + } // TODO: refactor recursion - function markdownifyStringsInObject( obj ) { + function markdownifyStringsInObject( obj, inline ) { if( !obj ) return; + + inline = inline === undefined || inline; + if( Object.prototype.toString.call( obj ) === '[object Array]' ) { - obj.forEach(function(elem){ - markdownifyStringsInObject( elem ); + 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') { - if( obj._isAMomentObject || obj.safe ) - return; Object.keys( obj ).forEach(function(key) { var sub = obj[key]; - if( typeof sub === 'string' || sub instanceof String ) - obj[key] = MD( obj[key] ); + if( typeof sub === 'string' || sub instanceof String ) { + if( key !== 'url' ) + obj[key] = inline ? MDIN( obj[key] ) : MD( obj[key] ); + } + else + markdownifyStringsInObject( sub ); }); } } Object.keys( ret ).forEach(function(member){ - markdownifyStringsInObject( that[ member ] ); + markdownifyStringsInObject( ret[ member ] ); }); return ret; @@ -191,7 +209,8 @@ Definition of the FRESHResume class. Get the default (empty) sheet. */ FreshResume.default = function() { - return new FreshResume().open( PATH.join( __dirname, 'empty-fresh.json'), 'Empty' ); + return new FreshResume().open( + PATH.join( __dirname, 'empty-fresh.json'), 'Empty' ); } /** diff --git a/src/core/theme.js b/src/core/theme.js index 17a9dc5..26ddd65 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -136,10 +136,11 @@ Abstract theme representation. // 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' + // For each CSS file, get its corresponding HTML file + var idx = _.findIndex(fmts, function( fmt ) { + return fmt.pre === cssf.pre && fmt.ext === 'html' }); + cssf.action = null; fmts[ idx ].css = cssf.data; fmts[ idx ].cssPath = cssf.path; }); diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js index a560dd6..0aeb3b5 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -22,12 +22,11 @@ Underscore template generate for FluentCV. jst = jst.replace( delims.comment, ''); // Compile and run the template. TODO: avoid unnecessary recompiles. var compiled = _.template(jst); - - var mr = json.markdownify(); - var ret = compiled({ r: json.markdownify(), filt: opts.filters, + XML: require('xml-escape'), + RAW: json, cssInfo: cssInfo, headFragment: opts.headFragment || '' }); From 3dcf3c3974cd2e8dfbf0374f487812be9cbca2db Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 9 Dec 2015 04:32:39 -0500 Subject: [PATCH 22/29] Tweak Markdownification. --- src/core/fresh-resume.js | 9 +++++++-- src/eng/underscore-generator.js | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index afeaf42..4e8f488 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -78,7 +78,8 @@ Definition of the FRESHResume class. }, /** - + Create a copy of this resume in which all fields have been interpreted as + Markdown. */ FreshResume.prototype.markdownify = function() { @@ -108,7 +109,11 @@ Definition of the FRESHResume class. Object.keys( obj ).forEach(function(key) { var sub = obj[key]; if( typeof sub === 'string' || sub instanceof String ) { - if( key !== 'url' ) + 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 diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js index 0aeb3b5..329550f 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -23,7 +23,7 @@ Underscore template generate for FluentCV. // Compile and run the template. TODO: avoid unnecessary recompiles. var compiled = _.template(jst); var ret = compiled({ - r: json.markdownify(), + r: format === 'html' || format === 'pdf' ? json.markdownify() : json, filt: opts.filters, XML: require('xml-escape'), RAW: json, From 2abfe4426c4699de5c23de4b9961b347dae0d4d7 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 9 Dec 2015 04:32:48 -0500 Subject: [PATCH 23/29] Refactor. --- src/gen/template-generator.js | 153 +++++++++++++++++++++------------- 1 file changed, 96 insertions(+), 57 deletions(-) diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index f11b80e..0a65ff8 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -5,6 +5,8 @@ Template-based resume generator base for FluentCV. (function() { + + var FS = require( 'fs-extra' ) , _ = require( 'underscore' ) , MD = require( 'marked' ) @@ -15,6 +17,8 @@ Template-based resume generator base for FluentCV. , EXTEND = require('../utils/extend') , Theme = require('../core/theme'); + + // Default options. var _defaultOpts = { engine: 'underscore', @@ -46,90 +50,73 @@ Template-based resume generator base for FluentCV. } }; + + /** 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({ - /** outputFormat: html, txt, pdf, doc - templateFormat: html or txt - **/ + + init: function( outputFormat, templateFormat, cssFile ){ this._super( outputFormat ); this.tplFormat = templateFormat || outputFormat; }, - /** Default generation method for template-based generators. */ - invoke: function( rez, themeMarkup, cssInfo, opts ) { - // Compile and invoke the template! + + invoke: function( rez, themeMarkup, cssInfo, opts ) { this.opts = EXTEND( true, {}, _defaultOpts, opts ); mk = this.single( rez, themeMarkup, this.format, cssInfo, { } ); this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f )); return mk; - }, - /** Default generation method for template-based generators. */ + + + /** + Default generation method for template-based generators. + @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 ) { // Carry over options this.opts = EXTEND( true, { }, _defaultOpts, opts ); - // Verify the specified theme name/path - var tFolder = PATH.join( - PATH.parse( require.resolve('fluent-themes') ).dir, - this.opts.theme - ); - var exists = require('../utils/file-exists'); - if (!exists( tFolder )) { - tFolder = PATH.resolve( this.opts.theme ); - if (!exists( tFolder )) { - throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme}; - } - } - - var outFolder = PATH.parse(f).dir; - // Load the theme - var theme = opts.themeObj || new Theme().open( tFolder ); - - // Load theme and CSS data + var themeInfo = themeFromMoniker.call( this ); + var theme = themeInfo.theme; + var tFolder = themeInfo.folder; var tplFolder = PATH.join( tFolder, 'src' ); + var outFolder = PATH.parse(f).dir; var curFmt = theme.getFormat( this.format ); - var that = this; + + // "Generate": process individual files within the theme curFmt.files.forEach(function(tplInfo){ - if( tplInfo.action === 'transform' || tplInfo.action === null ) { - if( tplInfo.action === 'transform' ) { - var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null }; - var mk = that.single( rez, tplInfo.data, that.format, cssInfo, that.opts ); - that.onBeforeSave && (mk = that.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); - var thisFilePath = PATH.join(outFolder, tplInfo.orgPath); - try { - MKDIRP.sync( PATH.dirname(thisFilePath) ); - FS.writeFileSync( thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); - } - catch(ex) { - console.log(ex); - } + if( tplInfo.action === 'transform' ) { + transform.call( that, rez, f, tplInfo, theme, outFolder ); + } + else if( tplInfo.action === null ) { + var thisFilePath = PATH.join(outFolder, tplInfo.orgPath); + try { + MKDIRP.sync( PATH.dirname(thisFilePath) ); + FS.copySync( tplInfo.path, thisFilePath ); } - else if( tplInfo.action === null ) { - var thisFilePath = PATH.join(outFolder, tplInfo.orgPath); - try { - MKDIRP.sync( PATH.dirname(thisFilePath) ); - FS.copySync( tplInfo.path, thisFilePath ); - } - catch(ex) { - console.log(ex); - } + catch(ex) { + console.log(ex); } } }); - // Create symlinks + // 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); @@ -141,6 +128,8 @@ Template-based resume generator base for FluentCV. }, + + /** Perform a single resume JSON-to-DEST resume transformation. @param json A FRESH or JRS resume object. @@ -150,28 +139,72 @@ Template-based resume generator base for FluentCV. @param opts Options and passthrough data. */ single: function( json, jst, format, cssInfo, opts ) { - - // Freeze whitespace in the template. this.opts.freezeBreaks && ( jst = freeze(jst) ); - - // Apply the template. var eng = require( '../eng/' + opts.themeObj.engine + '-generator' ); var result = eng( json, jst, format, cssInfo, opts ); - - // Unfreeze whitespace. 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( + PATH.parse( require.resolve('fluent-themes') ).dir, + this.opts.theme + ); + var exists = require('../utils/file-exists'); + 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 + }; + } + + + + /** + Transform a single subfile. + */ + function transform( rez, f, tplInfo, theme, outFolder ) { + var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null }; + var mk = this.single( rez, tplInfo.data, this.format, cssInfo, this.opts ); + this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); + var thisFilePath = PATH.join( outFolder, tplInfo.orgPath ); + try { + MKDIRP.sync( PATH.dirname(thisFilePath) ); + FS.writeFileSync( thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); + } + catch(ex) { + console.log(ex); + } + } + + + /** Freeze newlines for protection against errant JST parsers. */ @@ -181,6 +214,8 @@ Template-based resume generator base for FluentCV. .replace( _reg.regR, _defaultOpts.rSym ); } + + /** Unfreeze newlines when the coast is clear. */ @@ -190,6 +225,8 @@ Template-based resume generator base for FluentCV. .replace( _reg.regSymN, '\n' ); } + + /** Regexes for linebreak preservation. */ @@ -200,4 +237,6 @@ Template-based resume generator base for FluentCV. regSymR: new RegExp( _defaultOpts.rSym, 'g' ) }; + + }()); From 03957923591a15cb3f6b10b20ad38135de240db9 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 9 Dec 2015 05:08:10 -0500 Subject: [PATCH 24/29] Restore canonical output filename. --- src/core/theme.js | 7 +++++-- src/gen/template-generator.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/theme.js b/src/core/theme.js index 26ddd65..bb86a06 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -81,6 +81,7 @@ Abstract theme representation. // 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' ); @@ -94,7 +95,7 @@ Abstract theme representation. // such as "/latex" or "/html", then that format is the output format // for all files within the folder. var pathInfo = PATH.parse(absPath); - var outFmt = ''; + var outFmt = '', isMajor = false; var portion = pathInfo.dir.replace(tplFolder,''); if( portion && portion.trim() ) { var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig; @@ -106,7 +107,8 @@ Abstract theme representation. // 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 ) + outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 ); + isMajor = true; } // We should have a valid output format now. @@ -119,6 +121,7 @@ Abstract theme representation. var obj = { action: 'transform', path: absPath, + major: isMajor, orgPath: PATH.relative(that.folder, absPath), ext: pathInfo.ext.slice(1), title: friendlyName( outFmt ), diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 0a65ff8..2db20b1 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -196,7 +196,7 @@ Template-based resume generator base for FluentCV. var thisFilePath = PATH.join( outFolder, tplInfo.orgPath ); try { MKDIRP.sync( PATH.dirname(thisFilePath) ); - FS.writeFileSync( thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); + FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); } catch(ex) { console.log(ex); From f7a3da0a4df4c6a512444b4e0a061e8492673221 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 9 Dec 2015 05:41:04 -0500 Subject: [PATCH 25/29] Add generator tests for all themes. --- tests/test-themes.js | 110 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 tests/test-themes.js diff --git a/tests/test-themes.js b/tests/test-themes.js new file mode 100644 index 0000000..d01b487 --- /dev/null +++ b/tests/test-themes.js @@ -0,0 +1,110 @@ + +var chai = require('chai') + , expect = chai.expect + , should = chai.should() + , path = require('path') + , _ = require('underscore') + , FRESHResume = require('../src/core/fresh-resume') + , FCMD = require( '../src/fluentcmd') + , validator = require('is-my-json-valid') + , COLORS = require('colors'); + +chai.config.includeStack = false; + +describe('Testing themes', function () { + + var _sheet; + + COLORS.setTheme({ + title: ['white','bold'], + info: process.platform === 'win32' ? 'gray' : ['white','dim'], + infoBold: ['white','dim'], + warn: 'yellow', + error: 'red', + guide: 'yellow', + status: 'gray',//['white','dim'], + useful: 'green', + }); + + it('HELLO-WORLD theme should generate without throwing an exception', function () { + function tryOpen() { + var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; + var dst = ['tests/sandbox/hello-world/resume.all']; + var opts = { + theme: 'hello-world', + format: 'FRESH', + prettify: true, + silent: false + }; + FCMD.verbs.build( src, dst, opts ); + } + tryOpen.should.not.Throw(); + }); + + it('COMPACT theme should generate without throwing an exception', function () { + function tryOpen() { + var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; + var dst = ['tests/sandbox/compact/resume.all']; + var opts = { + theme: 'compact', + format: 'FRESH', + prettify: true, + silent: false + }; + FCMD.verbs.build( src, dst, opts ); + } + tryOpen.should.not.Throw(); + }); + + it('MODERN theme should generate without throwing an exception', function () { + function tryOpen() { + var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; + var dst = ['tests/sandbox/modern/resume.all']; + var opts = { + theme: 'modern', + format: 'FRESH', + prettify: true, + silent: false + }; + FCMD.verbs.build( src, dst, opts ); + } + tryOpen.should.not.Throw(); + }); + + it('MINIMIST theme should generate without throwing an exception', function () { + function tryOpen() { + var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; + var dst = ['tests/sandbox/minimist/resume.all']; + var opts = { + theme: 'minimist', + format: 'FRESH', + prettify: true, + silent: false + }; + FCMD.verbs.build( src, dst, opts ); + } + tryOpen.should.not.Throw(); + }); + + it('AWESOME theme should generate without throwing an exception', function () { + function tryOpen() { + var src = ['node_modules/FRESCA/exemplar/jane-doe.json']; + var dst = ['tests/sandbox/awesome/resume.all']; + var opts = { + theme: 'awesome', + format: 'FRESH', + prettify: true, + silent: false + }; + FCMD.verbs.build( src, dst, opts ); + } + tryOpen.should.not.Throw(); + }); + +}); + +// describe('subtract', function () { +// it('should return -1 when passed the params (1, 2)', function () { +// expect(math.subtract(1, 2)).to.equal(-1); +// }); +// }); From 91aba3905030645decae313dd84a20b8eddd7ba5 Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 9 Dec 2015 21:44:18 -0500 Subject: [PATCH 26/29] Add file LINTing through JSHint. --- Gruntfile.js | 27 ++++++++++++++++++--------- package.json | 1 + 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 90941b6..fd9987b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,7 +1,7 @@ -'use strict'; - module.exports = function (grunt) { + 'use strict'; + var opts = { pkg: grunt.file.readJSON('package.json'), @@ -29,19 +29,28 @@ module.exports = function (grunt) { outdir: 'docs/' } } + }, + + jshint: { + options: { + laxcomma: true, + expr: true + }, + all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'] } }; grunt.initConfig( opts ); + grunt.loadNpmTasks('grunt-simple-mocha'); grunt.loadNpmTasks('grunt-contrib-yuidoc'); - grunt.registerTask('test', 'Test the FluentCV library.', function( config ) { - grunt.task.run( ['simplemocha:all'] ); - }); - grunt.registerTask('document', 'Generate FluentCV library documentation.', function( config ) { - grunt.task.run( ['yuidoc'] ); - }); - grunt.registerTask('default', [ 'test', 'yuidoc' ]); + grunt.loadNpmTasks('grunt-contrib-jshint'); + + grunt.registerTask('test', 'Test the FluentCV library.', + function( config ) { grunt.task.run( ['simplemocha:all'] ); }); + grunt.registerTask('document', 'Generate FluentCV library documentation.', + function( config ) { grunt.task.run( ['yuidoc'] ); }); + grunt.registerTask('default', [ 'jshint', 'test', 'yuidoc' ]); }; diff --git a/package.json b/package.json index 7932fe8..59b76ec 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "devDependencies": { "chai": "*", "grunt": "*", + "grunt-contrib-jshint": "^0.11.3", "grunt-contrib-yuidoc": "^0.10.0", "grunt-simple-mocha": "*", "is-my-json-valid": "^2.12.2", From 541198321e0372860e3edd0f6cba1f1cb6ab178b Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 9 Dec 2015 21:44:35 -0500 Subject: [PATCH 27/29] Fix JSHint warnings. --- src/core/fluent-date.js | 4 ++-- src/core/fresh-resume.js | 21 +++++++++++---------- src/core/jrs-resume.js | 8 ++++---- src/core/theme.js | 10 +++++----- src/eng/handlebars-generator.js | 2 +- src/eng/underscore-generator.js | 2 +- src/fluentcmd.js | 20 +++++++++++--------- src/gen/template-generator.js | 4 ++-- src/index.js | 8 ++++---- src/utils/class.js | 2 +- 10 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/core/fluent-date.js b/src/core/fluent-date.js index df57d72..7a3d967 100644 --- a/src/core/fluent-date.js +++ b/src/core/fluent-date.js @@ -34,8 +34,8 @@ FluentDate/*.prototype*/.fmt = function( dt ) { else if( /^\D+\s+\d{4}$/.test(dt) ) { // "Mar 2015" var parts = dt.split(' '); var month = (months[parts[0]] || abbr[parts[0]]); - var dt = parts[1] + '-' + (month < 10 ? '0' + month : month.toString()); - return moment( dt, 'YYYY-MM' ); + 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' ); diff --git a/src/core/fresh-resume.js b/src/core/fresh-resume.js index 4e8f488..a0f5287 100644 --- a/src/core/fresh-resume.js +++ b/src/core/fresh-resume.js @@ -55,7 +55,7 @@ Definition of the FRESHResume class. FS.writeFileSync( this.imp.fileName, FreshResume.stringify( newRep ), 'utf8' ); } return this; - } + }; FreshResume.prototype.dupe = function() { var rnew = new FreshResume(); @@ -75,7 +75,7 @@ Definition of the FRESHResume class. ) ? undefined : value; } return JSON.stringify( obj, replacer, 2 ); - }, + }; /** Create a copy of this resume in which all fields have been interpreted as @@ -87,7 +87,7 @@ Definition of the FRESHResume class. var ret = this.dupe(); function MDIN(txt){ - return MD(txt || '' ).replace(/^\s*\|\<\/p\>\s*$/gi, ''); + return MD(txt || '' ).replace(/^\s*

|<\/p>\s*$/gi, ''); } // TODO: refactor recursion @@ -120,6 +120,7 @@ Definition of the FRESHResume class. markdownifyStringsInObject( sub ); }); } + } Object.keys( ret ).forEach(function(member){ @@ -188,7 +189,7 @@ Definition of the FRESHResume class. */ FreshResume.prototype.updateData = function( str ) { this.clear( false ); - this.parse( str ) + this.parse( str ); return this; }; @@ -216,7 +217,7 @@ Definition of the FRESHResume class. FreshResume.default = function() { return new FreshResume().open( PATH.join( __dirname, 'empty-fresh.json'), 'Empty' ); - } + }; /** Add work experience to the sheet. @@ -245,9 +246,9 @@ Definition of the FRESHResume class. FreshResume.prototype.getProfile = function( socialNetwork ) { socialNetwork = socialNetwork.trim().toLowerCase(); return this.social && _.find( this.social, function(sn) { - return sn.network.trim().toLowerCase() === socialNetwork + return sn.network.trim().toLowerCase() === socialNetwork; }); - } + }; /** Return an array of profiles for the specified network, for when the user @@ -256,9 +257,9 @@ Definition of the FRESHResume class. FreshResume.prototype.getProfiles = function( socialNetwork ) { socialNetwork = socialNetwork.trim().toLowerCase(); return this.social && _.filter( this.social, function(sn){ - return sn.network.trim().toLowerCase() === socialNetwork + return sn.network.trim().toLowerCase() === socialNetwork; }); - } + }; /** Determine if the sheet includes a specific skill. @@ -277,7 +278,7 @@ Definition of the FRESHResume class. */ FreshResume.prototype.isValid = function( info ) { var schemaObj = require('FRESCA'); - var validator = require('is-my-json-valid') + var validator = require('is-my-json-valid'); var validate = validator( schemaObj, { // Note [1] formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } }); diff --git a/src/core/jrs-resume.js b/src/core/jrs-resume.js index b7bfba3..4ec8749 100644 --- a/src/core/jrs-resume.js +++ b/src/core/jrs-resume.js @@ -94,14 +94,14 @@ Definition of the JRSResume class. }); } return flatSkills; - }, + }; /** Update the sheet's raw data. TODO: remove/refactor */ JRSResume.prototype.updateData = function( str ) { this.clear( false ); - this.parse( str ) + this.parse( str ); return this; }; @@ -127,7 +127,7 @@ Definition of the JRSResume class. */ JRSResume.default = function() { return new JRSResume().open( PATH.join( __dirname, 'empty-jrs.json'), 'Empty' ); - } + }; /** Add work experience to the sheet. @@ -168,7 +168,7 @@ Definition of the JRSResume class. 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 validator = require('is-my-json-valid'); var validate = validator( schemaObj ); return validate( this ); }; diff --git a/src/core/theme.js b/src/core/theme.js index bb86a06..9d43efd 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -50,8 +50,8 @@ Abstract theme representation. } // 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 }; + 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; @@ -141,7 +141,7 @@ Abstract theme representation. .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' + return fmt.pre === cssf.pre && fmt.ext === 'html'; }); cssf.action = null; fmts[ idx ].css = cssf.data; @@ -203,7 +203,7 @@ Abstract theme representation. // 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 ) + outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 ); } // We should have a valid output format now. @@ -237,7 +237,7 @@ Abstract theme representation. .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' + return fmt.pre === cssf.pre && fmt.ext === 'html'; }); fmts[ idx ].css = cssf.data; fmts[ idx ].cssPath = cssf.path; diff --git a/src/eng/handlebars-generator.js b/src/eng/handlebars-generator.js index 807c891..4d04a2a 100644 --- a/src/eng/handlebars-generator.js +++ b/src/eng/handlebars-generator.js @@ -11,7 +11,7 @@ Handlebars template generate for FluentCV. module.exports = function( json, jst, format, cssInfo, opts ) { var template = HANDLEBARS.compile(jst); - return template( { r: json, filt: opts.filters, cssInfo: cssInfo, headFragment: opts.headFragment || '' } ) + return template( { r: json, filt: opts.filters, cssInfo: cssInfo, headFragment: opts.headFragment || '' } ); }; diff --git a/src/eng/underscore-generator.js b/src/eng/underscore-generator.js index 329550f..c2a0543 100644 --- a/src/eng/underscore-generator.js +++ b/src/eng/underscore-generator.js @@ -13,7 +13,7 @@ Underscore template generate for FluentCV. var delims = opts.themeObj.delimeters || opts.template; if( opts.themeObj.delimeters ) { delims = _.mapObject( delims, function(val,key) { - return new RegExp( val, "ig") + return new RegExp( val, "ig"); }); } _.templateSettings = delims; diff --git a/src/fluentcmd.js b/src/fluentcmd.js index cb8b6c9..2d5accb 100644 --- a/src/fluentcmd.js +++ b/src/fluentcmd.js @@ -76,7 +76,7 @@ Internal resume generation logic for FluentCV. targets.push.apply(targets, fmat === '.all' ? Object.keys( theTheme.formats ).map(function(k){ var z = theTheme.formats[k]; - return { file: to.replace(/all$/g,z.outFormat), fmt: z } + return { file: to.replace(/all$/g,z.outFormat), fmt: z }; }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); }); @@ -97,7 +97,8 @@ Internal resume generation logic for FluentCV. try { var f = targInfo.file , fType = targInfo.fmt.outFormat - , fName = path.basename(f, '.' + fType); + , fName = path.basename(f, '.' + fType) + , theFormat; // If targInfo.fmt.files exists, this theme has an explicit "files" // section in its theme.json file. @@ -107,7 +108,7 @@ Internal resume generation logic for FluentCV. targInfo.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + path.relative(process.cwd(), f ).useful.bold); - var theFormat = _fmts.filter( + theFormat = _fmts.filter( function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; theFormat.gen.generate( rez, f, _opts ); @@ -134,7 +135,7 @@ Internal resume generation logic for FluentCV. targInfo.fmt.outFormat.toUpperCase().useful.bold + ' resume: '.useful + path.relative(process.cwd(), f ).useful.bold); - var theFormat = _fmts.filter( + theFormat = _fmts.filter( function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists; theFormat.gen.generate( rez, f, _opts ); @@ -181,8 +182,9 @@ Internal resume generation logic for FluentCV. sheets.forEach( function( rep ) { + var rez; try { - var rez = JSON.parse( rep.raw ); + rez = JSON.parse( rep.raw ); } catch( ex ) { _log('Validating '.info + rep.file.infoBold + @@ -204,11 +206,11 @@ Internal resume generation logic for FluentCV. var isValid = false; var style = 'useful'; var errors = []; + var fmt = rez.meta && + (rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars'; try { - var fmt = rez.meta && - (rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars'; var validate = validator( schemas[ fmt ], { // Note [1] formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ @@ -272,8 +274,8 @@ Internal resume generation logic for FluentCV. dst = src || ['resume.json']; dst.forEach( function( t ) { var safeFormat = opts.format.toUpperCase(); - _log('Creating new '.useful +safeFormat.useful.bold+ ' resume: '.useful - + t.useful.bold); + _log('Creating new '.useful +safeFormat.useful.bold + + ' resume: '.useful + t.useful.bold); MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists; FLUENT[ safeFormat + 'Resume' ].default().save( t ); }); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 2db20b1..5c68832 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -37,10 +37,10 @@ Template-based resume generator base for FluentCV. 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, ''); }, + 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 } + '' + name + '' : name; } }, prettify: { // ← See https://github.com/beautify-web/js-beautify#options indent_size: 2, diff --git a/src/index.js b/src/index.js index b5dd466..e50315f 100644 --- a/src/index.js +++ b/src/index.js @@ -79,7 +79,7 @@ function logMsg( msg ) { } function getOpts( args ) { - var noPretty = args['nopretty'] || args.n; + var noPretty = args.nopretty || args.n; noPretty = noPretty && (noPretty === true || noPretty === 'true'); return { theme: args.t || 'modern', @@ -101,15 +101,15 @@ function handleError( ex ) { case 3: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in FRESH or JSON Resume format.'.guide; break; case 4: msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide + Object.keys( FCMD.verbs ).map( function(v, idx, ar) { - return (idx === ar.length - 1 ? 'or '.guide : '') - + v.toUpperCase().guide; + return (idx === ar.length - 1 ? 'or '.guide : '') + + v.toUpperCase().guide; }).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold; break; //case 4: msg = title + '\n' + ; break; case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break; case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break; case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break; - }; + } exitCode = ex.fluenterror; } diff --git a/src/utils/class.js b/src/utils/class.js index 2640865..9479e89 100644 --- a/src/utils/class.js +++ b/src/utils/class.js @@ -41,7 +41,7 @@ return ret; }; - })(name, prop[name]) : + })(name, prop[name]) : // jshint ignore:line prop[name]; } From 81276cf2cc52dd569fa69568b5eb5b7ec54dae7e Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 9 Dec 2015 23:09:33 -0500 Subject: [PATCH 28/29] Update README. --- README.md | 54 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e34a1a6..23d7203 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,51 @@ fluentCV ======== -*Generate beautiful, targeted resumes from your command line or shell.* +*Create polished technical résumés and CVs in multiple formats from your command +line or shell. See [FluentCV Desktop][7] for the desktop version. OS X ~ Windows +~ Linux.* ![](assets/fluentcv_cli_ubuntu.png) -FluentCV is a Swiss Army knife for resumes and CVs. Use it to: +FluentCV is a dev-friendly Swiss Army knife for resumes and CVs. Use it to: -1. **Generate** polished multiformat resumes in HTML, Word, Markdown, PDF, plain -text, JSON, and YAML formats—without violating DRY. +1. **Generate** HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, +YAML, print, smoke signal, carrier pigeon, and other arbitrary-format resumes +and CVs, from a single source of truth—without violating DRY. 2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats. 3. **Validate** resumes against either format. -Install it with NPM: +FluentCV supports both the [FRESH][fresca] and [JSON Resume][6] source formats. + +## Features + +- OS X, Linux, and Windows. +- Store your resume data as a durable, versionable JSON or YAML document. +- Generate polished resumes in multiple formats without violating [DRY][dry]. +- Output to HTML, Markdown, LaTeX, PDF, MS Word, JSON, YAML, plain text, or XML. +- Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and +[FCV Desktop][7]. +- Validate resumes against the FRESH or JSON Resume schema. +- Support for multiple input and output resumes. +- Use from your command line or [desktop][7]. +- Free and open-source through the MIT license. + +## Install + +Install FluentCV with NPM: ```bash [sudo] npm install fluentcv -g ``` Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for -your platform. - -## Features - -- Runs on OS X, Linux, and Windows. -- Store your resume data as a durable, versionable JSON or YAML document. -- Generate polished resumes in multiple formats without violating [DRY][dry]. -- Output to HTML, PDF, Markdown, MS Word, JSON, YAML, plain text, or XML. -- Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and -[FCV Desktop][7]. -- Validate resumes against the FRESH or JSON Resume schema. -- Support for multiple input and output resumes. -- Free and open-source through the MIT license. -- Forthcoming: StackOverflow and LinkedIn support. -- Forthcoming: More commands and themes. - -Looking for a desktop GUI version for Windows / OS X / Linux? Check out -[FluentCV Desktop][7]. +your platform. For LaTeX generation you'll need a valid LaTeX environment with +access to `xelatex` and similar. ## Getting Started To use FluentCV you'll need to create a valid resume in either [FRESH][fresca] or [JSON Resume][6] format. Then you can start using the command line tool. -There are three commands you should be aware of: +There are four basic commands you should be aware of: - `build` generates resumes in HTML, Word, Markdown, PDF, and other formats. Use it when you need to submit, upload, print, or email resumes in specific formats. @@ -87,6 +91,7 @@ Output Format | Ext | Notes ------------- | --- | ----- HTML | .html | A standard HTML 5 + CSS resume format that can be viewed in a browser, deployed to a website, etc. Markdown | .md | A structured Markdown document that can be used as-is or used to generate HTML. +LaTeX | .tex | A structured LaTeX document (or collection of documents). MS Word | .doc | A Microsoft Word office document. Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme. plain text | .txt | A formatted plain text document appropriate for emails or copy-paste. @@ -101,6 +106,7 @@ image | .png, .bmp | Forthcoming. FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then: 1. Install the latest official [wkhtmltopdf][3] binary for your platform. +2. Optionally install an updated LaTeX environment (LaTeX resumes only). 2. Install **fluentCV** with `[sudo] npm install fluentcv -g`. 3. You're ready to go. From 3805a36271058e7f11d1a42d4866ebdd7d46c39e Mon Sep 17 00:00:00 2001 From: devlinjd Date: Wed, 9 Dec 2015 23:30:53 -0500 Subject: [PATCH 29/29] Fix folder generation wrinkle. --- src/core/theme.js | 1 + src/gen/template-generator.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/theme.js b/src/core/theme.js index 9d43efd..3d971a0 100644 --- a/src/core/theme.js +++ b/src/core/theme.js @@ -44,6 +44,7 @@ Abstract theme representation. // then this theme declares its files explicitly. if( !!this.formats ) { formatsHash = loadExplicit.call( this ); + this.explicit = true; } else { formatsHash = loadImplicit.call( this ); diff --git a/src/gen/template-generator.js b/src/gen/template-generator.js index 5c68832..3788c8f 100644 --- a/src/gen/template-generator.js +++ b/src/gen/template-generator.js @@ -104,7 +104,7 @@ Template-based resume generator base for FluentCV. if( tplInfo.action === 'transform' ) { transform.call( that, rez, f, tplInfo, theme, outFolder ); } - else if( tplInfo.action === null ) { + else if( tplInfo.action === null && theme.explicit ) { var thisFilePath = PATH.join(outFolder, tplInfo.orgPath); try { MKDIRP.sync( PATH.dirname(thisFilePath) ); @@ -195,7 +195,7 @@ Template-based resume generator base for FluentCV. this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } )); var thisFilePath = PATH.join( outFolder, tplInfo.orgPath ); try { - MKDIRP.sync( PATH.dirname(thisFilePath) ); + MKDIRP.sync( PATH.dirname( tplInfo.major ? f : thisFilePath) ); FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } ); } catch(ex) {