mirror of
				https://github.com/JuanCanham/HackMyResume.git
				synced 2025-10-31 13:17:26 +00:00 
			
		
		
		
	
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | ||||
| node_modules/ | ||||
| tests/sandbox/ | ||||
| docs/ | ||||
|   | ||||
							
								
								
									
										22
									
								
								Gruntfile.js
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								Gruntfile.js
									
									
									
									
									
								
							| @@ -15,15 +15,33 @@ module.exports = function (grunt) { | ||||
|         reporter: 'spec' | ||||
|       }, | ||||
|       all: { src: ['tests/*.js'] } | ||||
|     }, | ||||
|  | ||||
|     yuidoc: { | ||||
|       compile: { | ||||
|         name: '<%= pkg.name %>', | ||||
|         description: '<%= pkg.description %>', | ||||
|         version: '<%= pkg.version %>', | ||||
|         url: '<%= pkg.homepage %>', | ||||
|         options: { | ||||
|           paths: 'src/', | ||||
|           //themedir: 'path/to/custom/theme/', | ||||
|           outdir: 'docs/' | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   }; | ||||
|  | ||||
|   grunt.initConfig( opts ); | ||||
|   grunt.loadNpmTasks('grunt-simple-mocha'); | ||||
|   grunt.registerTask('test', 'Test the FluentLib library.', function( config ) { | ||||
|   grunt.loadNpmTasks('grunt-contrib-yuidoc'); | ||||
|   grunt.registerTask('test', 'Test the FluentCV library.', function( config ) { | ||||
|     grunt.task.run( ['simplemocha:all'] ); | ||||
|   }); | ||||
|   grunt.registerTask('default', [ 'test' ]); | ||||
|   grunt.registerTask('document', 'Generate FluentCV library documentation.', function( config ) { | ||||
|     grunt.task.run( ['yuidoc'] ); | ||||
|   }); | ||||
|   grunt.registerTask('default', [ 'test', 'yuidoc' ]); | ||||
|  | ||||
| }; | ||||
|   | ||||
							
								
								
									
										182
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										182
									
								
								README.md
									
									
									
									
									
								
							| @@ -2,63 +2,133 @@ fluentCV | ||||
| ======== | ||||
| *Generate beautiful, targeted resumes from your command line or shell.* | ||||
|  | ||||
| FluentCV is a **hackable, data-driven, dev-friendly resume authoring tool** with support for HTML, Markdown, Word, PDF, plain text, smoke signal, carrier pigeon, and other arbitrary-format resumes and CVs. | ||||
|  | ||||
|  | ||||
|  | ||||
| Looking for a desktop GUI version with pretty timelines and graphs? Check out [FluentCV Desktop][7]. | ||||
| FluentCV is a 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. | ||||
| 2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats. | ||||
| 3. **Validate** resumes against either format. | ||||
|  | ||||
| Install it 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, YML, or XML document. | ||||
| - Generate multiple targeted resumes in multiple formats, based on your needs. | ||||
| - Output to HTML, PDF, Markdown, Word, JSON, YAML, XML, or a custom format. | ||||
| - Never update one piece of information in four different resumes again. | ||||
| - Compatible with the [JSON Resume standard][6] and [authoring tools][7]. | ||||
| - 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 themes! | ||||
| - Forthcoming: More commands and themes. | ||||
|  | ||||
| Looking for a desktop GUI version for Windows / OS X / Linux? Check out | ||||
| [FluentCV Desktop][7]. | ||||
|  | ||||
| ## 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: | ||||
|  | ||||
| - `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. | ||||
|  | ||||
|     ```bash | ||||
|     # fluentcv BUILD <INPUTS> TO <OUTPUTS> [-t THEME] | ||||
|     fluentcv BUILD resume.json TO out/resume.all | ||||
|     fluentcv BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all | ||||
|     ``` | ||||
|  | ||||
| - `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. | ||||
|  | ||||
|     ```bash | ||||
|     # fluentcv CONVERT <INPUTS> TO <OUTPUTS> | ||||
|     fluentcv CONVERT resume.json TO resume-jrs.json | ||||
|     fluentcv CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json | ||||
|     ``` | ||||
|  | ||||
| - `validate` validates the specified resume against either the FRESH or JSON | ||||
| Resume schema. Use it to make sure your resume data is sufficient and complete. | ||||
|  | ||||
|     ```bash | ||||
|     # fluentcv VALIDATE <INPUTS> | ||||
|     fluentcv VALIDATE resume.json | ||||
|     fluentcv VALIDATE r1.json r2.json r3.json | ||||
|     ``` | ||||
|  | ||||
| ## Supported Output Formats | ||||
|  | ||||
| FluentCV supports these output formats: | ||||
|  | ||||
| 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. | ||||
| 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. | ||||
| JSON | .json | A JSON representation of the resume. | ||||
| YAML | .yml | A YAML representation of the resume. | ||||
| RTF | .rtf | Forthcoming. | ||||
| Textile | .textile | Forthcoming. | ||||
| image | .png, .bmp | Forthcoming. | ||||
|  | ||||
| ## Install | ||||
|  | ||||
| FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then: | ||||
|  | ||||
| 1. (Optional, for PDF support) Install the latest official [wkhtmltopdf][3] binary for your platform. | ||||
| 2. Install **fluentCV** by running `npm install fluentcv -g`. | ||||
| 1. Install the latest official [wkhtmltopdf][3] binary for your platform. | ||||
| 2. Install **fluentCV** with `[sudo] npm install fluentcv -g`. | ||||
| 3. You're ready to go. | ||||
|  | ||||
| ## Use | ||||
|  | ||||
| Assuming you've got a JSON-formatted resume handy, generating resumes in different formats and combinations easy. Just run: | ||||
| Assuming you've got a JSON-formatted resume handy, generating resumes in | ||||
| different formats and combinations easy. Just run: | ||||
|  | ||||
| ```bash | ||||
| fluentcv [inputs] [outputs] [-t theme]. | ||||
| fluentcv BUILD <INPUTS> <OUTPUTS> [-t theme]. | ||||
| ``` | ||||
|  | ||||
| Where `[inputs]` is one or more .json resume files, separated by spaces; `[outputs]` is one or more destination resumes, each prefaced with the `-o` option; and `[theme]` is the desired theme. For example: | ||||
| Where `<INPUTS>` is one or more .json resume files, separated by spaces; | ||||
| `<OUTPUTS>` is one or more destination resumes, and `<THEME>` is the desired | ||||
| theme (default to Modern). For example: | ||||
|  | ||||
| ```bash | ||||
| # Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.) | ||||
| fluentcv resume.json -o out/resume.all -t modern | ||||
| fluentcv build resume.json -o out/resume.all -t modern | ||||
|  | ||||
| # Generate a specific resume format | ||||
| fluentcv resume.json -o out/resume.html | ||||
| fluentcv resume.json -o out/resume.pdf | ||||
| fluentcv resume.json -o out/resume.md | ||||
| fluentcv resume.json -o out/resume.doc | ||||
| fluentcv resume.json -o out/resume.json | ||||
| fluentcv resume.json -o out/resume.txt | ||||
| fluentcv resume.json -o out/resume.yml | ||||
| fluentcv build resume.json TO out/resume.html | ||||
| fluentcv build resume.json TO out/resume.pdf | ||||
| fluentcv build resume.json TO out/resume.md | ||||
| fluentcv build resume.json TO out/resume.doc | ||||
| fluentcv build resume.json TO out/resume.json | ||||
| fluentcv build resume.json TO out/resume.txt | ||||
| fluentcv build resume.json TO out/resume.yml | ||||
|  | ||||
| # Specify 2 inputs and 3 outputs | ||||
| fluentcv in1.json in2.json -o out.html -o out.doc -o out.pdf | ||||
| fluentcv build in1.json in2.json TO out.html out.doc out.pdf | ||||
| ``` | ||||
|  | ||||
| You should see something to the effect of: | ||||
|  | ||||
| ``` | ||||
| *** FluentCV v0.7.2 *** | ||||
| *** FluentCV v0.9.0 *** | ||||
| Reading JSON resume: foo/resume.json | ||||
| Applying MODERN Theme (7 formats) | ||||
| Generating HTML resume: out/resume.html | ||||
| @@ -77,11 +147,11 @@ Generating YAML resume: out/resume.yml | ||||
| You can specify a predefined or custom theme via the optional `-t` parameter. For a predefined theme, include the theme name. For a custom theme, include the path to the custom theme's folder. | ||||
|  | ||||
| ```bash | ||||
| fluentcv resume.json -t modern | ||||
| fluentcv resume.json -t ~/foo/bar/my-custom-theme/ | ||||
| fluentcv build resume.json -t modern | ||||
| fluentcv build resume.json -t ~/foo/bar/my-custom-theme/ | ||||
| ``` | ||||
|  | ||||
| As of v0.7.2, available predefined themes are `modern`, `minimist`, and `hello-world`, and `compact`. | ||||
| As of v0.9.0, available predefined themes are `modern`, `minimist`, and `hello-world`, and `compact`. | ||||
|  | ||||
| ### Merging resumes | ||||
|  | ||||
| @@ -89,13 +159,13 @@ You can **merge multiple resumes together** by specifying them in order from mos | ||||
|  | ||||
| ```bash | ||||
| # Merge specific.json onto base.json and generate all formats | ||||
| fluentcv base.json specific.json -o resume.all | ||||
| fluentcv build base.json specific.json -o resume.all | ||||
| ``` | ||||
|  | ||||
| This can be useful for overriding a base (generic) resume with information from a specific (targeted) resume. For example, you might override your generic catch-all "software developer" resume with specific details from your targeted "game developer" resume, or combine two partial resumes into a "complete" resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge: | ||||
|  | ||||
| ```bash | ||||
| fluentcv in1.json in2.json in3.json in4.json -o out.html -o out.doc | ||||
| fluentcv build in1.json in2.json in3.json in4.json TO out.html out.doc | ||||
| Reading JSON resume: in1.json | ||||
| Reading JSON resume: in2.json | ||||
| Reading JSON resume: in3.json | ||||
| @@ -111,14 +181,14 @@ You can specify **multiple output targets** and FluentCV will build them: | ||||
|  | ||||
| ```bash | ||||
| # Generate out1.doc, out1.pdf, and foo.txt from me.json. | ||||
| fluentcv me.json -o out1.doc -o out1.pdf -o foo.txt | ||||
| fluentcv build me.json -o out1.doc -o out1.pdf -o foo.txt | ||||
| ``` | ||||
|  | ||||
| You can also omit the output file(s) and/or theme completely: | ||||
|  | ||||
| ```bash | ||||
| # Equivalent to "fluentcv resume.json resume.all -t modern" | ||||
| fluentcv resume.json | ||||
| fluentcv build resume.json | ||||
| ``` | ||||
|  | ||||
| ### Using .all | ||||
| @@ -127,17 +197,52 @@ The special `.all` extension tells FluentCV to generate all supported output for | ||||
|  | ||||
| ```bash | ||||
| # Generate all resume formats (HTML, PDF, DOC, TXT, etc.) | ||||
| fluentcv me.json -o out/resume.all | ||||
| fluentcv build me.json -o out/resume.all | ||||
| ``` | ||||
|  | ||||
| ..tells FluentCV to read `me.json` and generate `out/resume.md`, `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`. | ||||
|  | ||||
| ### Prettifying | ||||
| ### Validating | ||||
|  | ||||
| FluentCV applies [js-beautify][10]-style HTML prettification by default to HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag can be used: | ||||
| FluentCV can also validate your resumes against either the [FRESH / | ||||
| FRESCA][fresca] or [JSON Resume][6] formats. To validate one or more existing | ||||
| resumes, use the `validate` command: | ||||
|  | ||||
| ```bash | ||||
| fluentcv resume.json out.all --nopretty | ||||
| # Validate myresume.json against either the FRESH or JSON Resume schema. | ||||
| fluentcv validate resumeA.json resumeB.json | ||||
| ``` | ||||
|  | ||||
| FluentCV will validate each specified resume in turn: | ||||
|  | ||||
| ```bash | ||||
| *** FluentCV v0.9.0 *** | ||||
| Validating JSON resume: resumeA.json (INVALID) | ||||
| Validating JSON resume: resumeB.json (VALID) | ||||
| ``` | ||||
|  | ||||
| ### Converting | ||||
|  | ||||
| FluentCV can convert between the [FRESH][fresca] and [JSON Resume][6] formats. | ||||
| Just run: | ||||
|  | ||||
| ```bash | ||||
| fluentcv CONVERT <INPUTS> <OUTPUTS> | ||||
| ``` | ||||
|  | ||||
| where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and | ||||
| <OUTPUTS> is a corresponding list of output file names. FluentCV will autodetect | ||||
| the format (FRESH or JRS) of each input resume and convert it to the other | ||||
| format (JRS or FRESH). | ||||
|  | ||||
| ### Prettifying | ||||
|  | ||||
| FluentCV applies [js-beautify][10]-style HTML prettification by default to | ||||
| HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag | ||||
| can be used: | ||||
|  | ||||
| ```bash | ||||
| fluentcv generate resume.json out.all --nopretty | ||||
| ``` | ||||
|  | ||||
| ### Silent Mode | ||||
| @@ -145,8 +250,8 @@ fluentcv resume.json out.all --nopretty | ||||
| Use `-s` or `--silent` to run in silent mode: | ||||
|  | ||||
| ```bash | ||||
| fluentcv resume.json -o someFile.all -s | ||||
| fluentcv resume.json -o someFile.all --silent | ||||
| fluentcv generate resume.json -o someFile.all -s | ||||
| fluentcv generate resume.json -o someFile.all --silent | ||||
| ``` | ||||
|  | ||||
| ## License | ||||
| @@ -163,3 +268,6 @@ MIT. Go crazy. See [LICENSE.md][1] for details. | ||||
| [8]: https://youtu.be/N9wsjroVlu8 | ||||
| [9]: https://api.jquery.com/jquery.extend/ | ||||
| [10]: https://github.com/beautify-web/js-beautify | ||||
| [fresh]: https://github.com/fluentdesk/FRESH | ||||
| [fresca]: https://github.com/fluentdesk/FRESCA | ||||
| [dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 53 KiB | 
							
								
								
									
										17
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "fluentcv", | ||||
|   "version": "0.8.0", | ||||
|   "version": "0.9.0", | ||||
|   "description": "Generate beautiful, targeted resumes from your command line, shell, or in Javascript with Node.js.", | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
| @@ -10,7 +10,15 @@ | ||||
|     "resume", | ||||
|     "CV", | ||||
|     "portfolio", | ||||
|     "Markdown" | ||||
|     "employment", | ||||
|     "career", | ||||
|     "Markdown", | ||||
|     "JSON", | ||||
|     "Word", | ||||
|     "PDF", | ||||
|     "YAML", | ||||
|     "HTML", | ||||
|     "CLI" | ||||
|   ], | ||||
|   "author": "James M. Devlin", | ||||
|   "license": "MIT", | ||||
| @@ -24,7 +32,9 @@ | ||||
|   }, | ||||
|   "homepage": "https://github.com/fluentdesk/fluentcv", | ||||
|   "dependencies": { | ||||
|     "fluent-themes": "0.4.0-beta", | ||||
|     "FRESCA": "fluentdesk/FRESCA#v0.1.0", | ||||
|     "colors": "^1.1.2", | ||||
|     "fluent-themes": "0.5.0-beta", | ||||
|     "fs-extra": "^0.24.0", | ||||
|     "html": "0.0.10", | ||||
|     "is-my-json-valid": "^2.12.2", | ||||
| @@ -41,6 +51,7 @@ | ||||
|   "devDependencies": { | ||||
|     "chai": "*", | ||||
|     "grunt": "*", | ||||
|     "grunt-contrib-yuidoc": "^0.10.0", | ||||
|     "grunt-simple-mocha": "*", | ||||
|     "is-my-json-valid": "^2.12.2", | ||||
|     "mocha": "*", | ||||
|   | ||||
							
								
								
									
										291
									
								
								src/core/convert.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								src/core/convert.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,291 @@ | ||||
| /** | ||||
| FRESH to JSON Resume conversion routiens. | ||||
| @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk | ||||
| */ | ||||
|  | ||||
| (function(){ | ||||
|  | ||||
|   /** | ||||
|   Convert between FRESH and JRS resume/CV formats. | ||||
|   @class FRESHConverter | ||||
|   */ | ||||
|   var FRESHConverter = module.exports = { | ||||
|  | ||||
|  | ||||
|     /** | ||||
|     Convert from JSON Resume format to FRESH. | ||||
|     */ | ||||
|     toFRESH: function( src, foreign ) { | ||||
|  | ||||
|       foreign = (foreign === undefined || foreign === null) ? true : foreign; | ||||
|  | ||||
|       return { | ||||
|  | ||||
|         name: src.basics.name, | ||||
|  | ||||
|         info: { | ||||
|           label: src.basics.label, | ||||
|           class: src.basics.class, // <--> round-trip | ||||
|           image: src.basics.picture, | ||||
|           brief: src.basics.summary | ||||
|         }, | ||||
|  | ||||
|         contact: { | ||||
|           email: src.basics.email, | ||||
|           phone: src.basics.phone, | ||||
|           website: src.basics.website, | ||||
|           other: src.basics.other // <--> round-trip | ||||
|         }, | ||||
|  | ||||
|         meta: meta( true, src.meta ), | ||||
|  | ||||
|         location: { | ||||
|           city: src.basics.location.city, | ||||
|           region: src.basics.location.region, | ||||
|           country: src.basics.location.countryCode, | ||||
|           code: src.basics.location.postalCode, | ||||
|           address: src.basics.location.address | ||||
|         }, | ||||
|  | ||||
|         employment: { | ||||
|           history: src.work.map( function( job ) { | ||||
|             return { | ||||
|               position: job.position, | ||||
|               employer: job.company, | ||||
|               summary: job.summary, | ||||
|               current: (!job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current') || undefined, | ||||
|               start: job.startDate, | ||||
|               end: job.endDate, | ||||
|               url: job.website, | ||||
|               keywords: "", | ||||
|               highlights: job.highlights | ||||
|             }; | ||||
|           }) | ||||
|         }, | ||||
|  | ||||
|         education: { | ||||
|           history: src.education.map(function(edu){ | ||||
|             return { | ||||
|               institution: edu.institution, | ||||
|               start: edu.startDate, | ||||
|               end: edu.endDate, | ||||
|               grade: edu.gpa, | ||||
|               curriculum: edu.courses, | ||||
|               url: edu.website || edu.url || null, | ||||
|               summary: null, | ||||
|               area: edu.area, | ||||
|               studyType: edu.studyType | ||||
|             }; | ||||
|           }) | ||||
|         }, | ||||
|  | ||||
|         service: { | ||||
|           history: src.volunteer.map(function(vol) { | ||||
|             return { | ||||
|               type: 'volunteer', | ||||
|               position: vol.position, | ||||
|               organization: vol.organization, | ||||
|               start: vol.startDate, | ||||
|               end: vol.endDate, | ||||
|               url: vol.website, | ||||
|               summary: vol.summary, | ||||
|               highlights: vol.highlights | ||||
|             }; | ||||
|           }) | ||||
|         }, | ||||
|  | ||||
|         skills: skillsToFRESH( src.skills ), | ||||
|  | ||||
|         writing: src.publications.map(function(pub){ | ||||
|           return { | ||||
|             title: pub.name, | ||||
|             flavor: undefined, | ||||
|             publisher: pub.publisher, | ||||
|             url: pub.website, | ||||
|             date: pub.releaseDate, | ||||
|             summary: pub.summary | ||||
|           }; | ||||
|         }), | ||||
|  | ||||
|         recognition: src.awards.map(function(awd){ | ||||
|           return { | ||||
|             title: awd.title, | ||||
|             date: awd.date, | ||||
|             summary: awd.summary, | ||||
|             from: awd.awarder, | ||||
|             url: null | ||||
|           }; | ||||
|         }), | ||||
|  | ||||
|         social: src.basics.profiles.map(function(pro){ | ||||
|           return { | ||||
|             label: pro.network, | ||||
|             network: pro.network, | ||||
|             url: pro.url, | ||||
|             user: pro.username | ||||
|           }; | ||||
|         }), | ||||
|  | ||||
|         interests: src.interests, | ||||
|         references: src.references, | ||||
|         languages: src.languages, | ||||
|         disposition: src.disposition // <--> round-trip | ||||
|       }; | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|     Convert from FRESH format to JSON Resume. | ||||
|     @param foreign True if non-JSON-Resume properties should be included in | ||||
|     the result, false if those properties should be excluded. | ||||
|     */ | ||||
|     toJRS: function( src, foreign ) { | ||||
|  | ||||
|       foreign = (foreign === undefined || foreign === null) ? false : foreign; | ||||
|  | ||||
|       return { | ||||
|  | ||||
|         basics: { | ||||
|           name: src.name, | ||||
|           label: src.info.label, | ||||
|           class: foreign ? src.info.class : undefined, | ||||
|           summary: src.info.brief, | ||||
|           website: src.contact.website, | ||||
|           phone: src.contact.phone, | ||||
|           email: src.contact.email, | ||||
|           picture: src.info.image, | ||||
|           location: { | ||||
|             address: src.location.address, | ||||
|             postalCode: src.location.code, | ||||
|             city: src.location.city, | ||||
|             countryCode: src.location.country, | ||||
|             region: src.location.region | ||||
|           }, | ||||
|           profiles: src.social.map(function(soc){ | ||||
|             return { | ||||
|               network: soc.network, | ||||
|               username: soc.user, | ||||
|               url: soc.url | ||||
|             }; | ||||
|           }) | ||||
|         }, | ||||
|  | ||||
|         work: src.employment.history.map(function(emp){ | ||||
|           return { | ||||
|             company: emp.employer, | ||||
|             website: emp.url, | ||||
|             position: emp.position, | ||||
|             startDate: emp.start, | ||||
|             endDate: emp.end, | ||||
|             summary: emp.summary, | ||||
|             highlights: emp.highlights | ||||
|           }; | ||||
|         }), | ||||
|  | ||||
|         education: src.education.history.map(function(edu){ | ||||
|           return { | ||||
|             institution: edu.institution, | ||||
|             gpa: edu.grade, | ||||
|             courses: edu.curriculum, | ||||
|             startDate: edu.start, | ||||
|             endDate: edu.end, | ||||
|             area: edu.area, | ||||
|             studyType: edu.studyType | ||||
|           }; | ||||
|         }), | ||||
|  | ||||
|         skills: skillsToJRS( src.skills ), | ||||
|  | ||||
|         volunteer: src.service.history.map(function(srv){ | ||||
|           return { | ||||
|             flavor: foreign ? srv.flavor : undefined, | ||||
|             organization: srv.organization, | ||||
|             position: srv.position, | ||||
|             startDate: srv.start, | ||||
|             endDate: srv.end, | ||||
|             website: srv.url, | ||||
|             summary: srv.summary, | ||||
|             highlights: srv.highlights | ||||
|           }; | ||||
|         }), | ||||
|  | ||||
|         awards: src.recognition.map(function(awd){ | ||||
|           return { | ||||
|             flavor: foreign ? awd.flavor : undefined, | ||||
|             url: foreign ? awd.url: undefined, | ||||
|             title: awd.title, | ||||
|             date: awd.date, | ||||
|             awarder: awd.from, | ||||
|             summary: awd.summary | ||||
|           }; | ||||
|         }), | ||||
|  | ||||
|         publications: src.writing.map(function(pub){ | ||||
|           return { | ||||
|             name: pub.title, | ||||
|             publisher: pub.publisher, | ||||
|             releaseDate: pub.date, | ||||
|             website: pub.url, | ||||
|             summary: pub.summary | ||||
|           }; | ||||
|         }), | ||||
|  | ||||
|         interests: src.interests, | ||||
|         references: src.references, | ||||
|         samples: foreign ? src.samples : undefined, | ||||
|         disposition: foreign ? src.disposition : undefined, | ||||
|         languages: src.languages | ||||
|  | ||||
|       }; | ||||
|  | ||||
|     } | ||||
|  | ||||
|   }; | ||||
|  | ||||
|   function meta( direction, obj ) { | ||||
|     if( direction ) { | ||||
|       obj = obj || { }; | ||||
|       obj.format = obj.format || "FRESH@0.1.0"; | ||||
|       obj.version = obj.version || "0.1.0"; | ||||
|     } | ||||
|     return obj; | ||||
|   } | ||||
|  | ||||
|   function skillsToFRESH( skills ) { | ||||
|  | ||||
|     return { | ||||
|       sets: skills.map(function(set) { | ||||
|         return { | ||||
|           name: set.name, | ||||
|           level: set.level, | ||||
|           skills: set.keywords | ||||
|         }; | ||||
|       }) | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   function skillsToJRS( skills ) { | ||||
|     var ret = []; | ||||
|     if( skills.sets && skills.sets.length ) { | ||||
|       ret = skills.sets.map(function(set){ | ||||
|         return { | ||||
|           name: set.name, | ||||
|           level: set.level, | ||||
|           keywords: set.skills | ||||
|         }; | ||||
|       }); | ||||
|     } | ||||
|     else if( skills.list ) { | ||||
|       ret = skills.list.map(function(sk){ | ||||
|         return { | ||||
|           name: sk.name, | ||||
|           level: sk.level, | ||||
|           keywords: sk.keywords | ||||
|         }; | ||||
|       }); | ||||
|     } | ||||
|     return ret; | ||||
|   } | ||||
|  | ||||
|  | ||||
|  | ||||
| }()); | ||||
							
								
								
									
										310
									
								
								src/core/fresh-resume.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										310
									
								
								src/core/fresh-resume.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,310 @@ | ||||
| /** | ||||
| Definition of the FRESHResume class. | ||||
| @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk | ||||
| */ | ||||
|  | ||||
| (function() { | ||||
|  | ||||
|   var FS = require('fs') | ||||
|     , extend = require('../utils/extend') | ||||
|     , validator = require('is-my-json-valid') | ||||
|     , _ = require('underscore') | ||||
|     , PATH = require('path') | ||||
|     , moment = require('moment') | ||||
|     , CONVERTER = require('./convert'); | ||||
|  | ||||
|   /** | ||||
|   A FRESH-style resume in JSON or YAML. | ||||
|   @class FreshResume | ||||
|   */ | ||||
|   function FreshResume() { | ||||
|  | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   Open and parse the specified FRESH resume sheet. Merge the JSON object model | ||||
|   onto this Sheet instance with extend() and convert sheet dates to a safe & | ||||
|   consistent format. Then sort each section by startDate descending. | ||||
|   */ | ||||
|   FreshResume.prototype.open = function( file, title ) { | ||||
|     this.imp = { fileName: file }; | ||||
|     this.imp.raw = FS.readFileSync( file, 'utf8' ); | ||||
|     return this.parse( this.imp.raw, title ); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Save the sheet to disk (for environments that have disk access). | ||||
|   */ | ||||
|   FreshResume.prototype.save = function( filename ) { | ||||
|     this.imp.fileName = filename || this.imp.fileName; | ||||
|     FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' ); | ||||
|     return this; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Save the sheet to disk in a specific format, either FRESH or JSON Resume. | ||||
|   */ | ||||
|   FreshResume.prototype.saveAs = function( filename, format ) { | ||||
|     this.imp.fileName = filename || this.imp.fileName; | ||||
|     if( format !== 'JRS' ) { | ||||
|       FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' ); | ||||
|     } | ||||
|     else { | ||||
|       var newRep = CONVERTER.toJRS( this ); | ||||
|       FS.writeFileSync( this.imp.fileName, FreshResume.stringify( newRep ), 'utf8' ); | ||||
|     } | ||||
|     return this; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   Convert the supplied object to a JSON string, sanitizing meta-properties along | ||||
|   the way. | ||||
|   */ | ||||
|   FreshResume.stringify = function( obj ) { | ||||
|     function replacer( key,value ) { // Exclude these keys from stringification | ||||
|       return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', | ||||
|         'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'], | ||||
|         function( val ) { return key.trim() === val; } | ||||
|       ) ? undefined : value; | ||||
|     } | ||||
|     return JSON.stringify( obj, replacer, 2 ); | ||||
|   }, | ||||
|  | ||||
|   /** | ||||
|   Convert this object to a JSON string, sanitizing meta-properties along the | ||||
|   way. Don't override .toString(). | ||||
|   */ | ||||
|   FreshResume.prototype.stringify = function() { | ||||
|     return FreshResume.stringify( this ); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Open and parse the specified JSON resume sheet. Merge the JSON object model | ||||
|   onto this Sheet instance with extend() and convert sheet dates to a safe & | ||||
|   consistent format. Then sort each section by startDate descending. | ||||
|   */ | ||||
|   FreshResume.prototype.parse = function( stringData, opts ) { | ||||
|  | ||||
|     // Parse the incoming JSON representation | ||||
|     var rep = JSON.parse( stringData ); | ||||
|  | ||||
|     // Convert JSON Resume to FRESH if necessary | ||||
|     if( rep.basics ) { | ||||
|       rep = CONVERTER.toFRESH( rep ); | ||||
|       rep.imp = rep.imp || { }; | ||||
|       rep.imp.orgFormat = 'JRS'; | ||||
|     } | ||||
|  | ||||
|     // Now apply the resume representation onto this object | ||||
|     extend( true, this, rep ); | ||||
|  | ||||
|     // Set up metadata | ||||
|     opts = opts || { }; | ||||
|     if( opts.imp === undefined || opts.imp ) { | ||||
|       this.imp = this.imp || { }; | ||||
|       this.imp.title = (opts.title || this.imp.title) || this.name; | ||||
|     } | ||||
|     // Parse dates, sort dates, and calculate computed values | ||||
|     (opts.date === undefined || opts.date) && _parseDates.call( this ); | ||||
|     (opts.sort === undefined || opts.sort) && this.sort(); | ||||
|     (opts.compute === undefined || opts.compute) && (this.computed = { | ||||
|        numYears: this.duration(), | ||||
|        keywords: this.keywords() | ||||
|     }); | ||||
|     return this; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Return a unique list of all keywords across all skills. | ||||
|   */ | ||||
|   FreshResume.prototype.keywords = function() { | ||||
|     var flatSkills = []; | ||||
|     this.skills && this.skills.length && | ||||
|       (flatSkills = this.skills.map(function(sk) { return sk.name;  })); | ||||
|     return flatSkills; | ||||
|   }, | ||||
|  | ||||
|   /** | ||||
|   Update the sheet's raw data. TODO: remove/refactor | ||||
|   */ | ||||
|   FreshResume.prototype.updateData = function( str ) { | ||||
|     this.clear( false ); | ||||
|     this.parse( str ) | ||||
|     return this; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Reset the sheet to an empty state. | ||||
|   */ | ||||
|   FreshResume.prototype.clear = function( clearMeta ) { | ||||
|     clearMeta = ((clearMeta === undefined) && true) || clearMeta; | ||||
|     clearMeta && (delete this.imp); | ||||
|     delete this.computed; // Don't use Object.keys() here | ||||
|     delete this.employment; | ||||
|     delete this.service; | ||||
|     delete this.education; | ||||
|     //delete this.awards; | ||||
|     delete this.publications; | ||||
|     //delete this.interests; | ||||
|     delete this.skills; | ||||
|     delete this.social; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Get the default (empty) sheet. | ||||
|   */ | ||||
|   FreshResume.default = function() { | ||||
|     return new FreshResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   Add work experience to the sheet. | ||||
|   */ | ||||
|   FreshResume.prototype.add = function( moniker ) { | ||||
|     var defSheet = FreshResume.default(); | ||||
|     var newObject = $.extend( true, {}, defSheet[ moniker ][0] ); | ||||
|     this[ moniker ] = this[ moniker ] || []; | ||||
|     this[ moniker ].push( newObject ); | ||||
|     return newObject; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Determine if the sheet includes a specific social profile (eg, GitHub). | ||||
|   */ | ||||
|   FreshResume.prototype.hasProfile = function( socialNetwork ) { | ||||
|     socialNetwork = socialNetwork.trim().toLowerCase(); | ||||
|     return this.social && _.some( this.social, function(p) { | ||||
|       return p.network.trim().toLowerCase() === socialNetwork; | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Determine if the sheet includes a specific skill. | ||||
|   */ | ||||
|   FreshResume.prototype.hasSkill = function( skill ) { | ||||
|     skill = skill.trim().toLowerCase(); | ||||
|     return this.skills && _.some( this.skills, function(sk) { | ||||
|       return sk.keywords && _.some( sk.keywords, function(kw) { | ||||
|         return kw.trim().toLowerCase() === skill; | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Validate the sheet against the FRESH Resume schema. | ||||
|   */ | ||||
|   FreshResume.prototype.isValid = function( info ) { | ||||
|     var schemaObj = require('FRESCA'); | ||||
|     var validator = require('is-my-json-valid') | ||||
|     var validate = validator( schemaObj, { // Note [1] | ||||
|       formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } | ||||
|     }); | ||||
|     var ret = validate( this ); | ||||
|     if( !ret ) { | ||||
|       this.imp = this.imp || { }; | ||||
|       this.imp.validationErrors = validate.errors; | ||||
|     } | ||||
|     return ret; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Calculate the total duration of the sheet. Assumes this.work has been sorted | ||||
|   by start date descending, perhaps via a call to Sheet.sort(). | ||||
|   @returns The total duration of the sheet's work history, that is, the number | ||||
|   of years between the start date of the earliest job on the resume and the | ||||
|   *latest end date of all jobs in the work history*. This last condition is for | ||||
|   sheets that have overlapping jobs. | ||||
|   */ | ||||
|   FreshResume.prototype.duration = function() { | ||||
|     if( this.employment.history && this.employment.history.length ) { | ||||
|       var careerStart = this.employment.history[ this.employment.history.length - 1].safe.start; | ||||
|       if ((typeof careerStart === 'string' || careerStart instanceof String) && | ||||
|           !careerStart.trim()) | ||||
|         return 0; | ||||
|       var careerLast = _.max( this.employment.history, function( w ) { | ||||
|         return w.safe.end.unix(); | ||||
|       }).safe.end; | ||||
|       return careerLast.diff( careerStart, 'years' ); | ||||
|     } | ||||
|     return 0; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Sort dated things on the sheet by start date descending. Assumes that dates | ||||
|   on the sheet have been processed with _parseDates(). | ||||
|   */ | ||||
|   FreshResume.prototype.sort = function( ) { | ||||
|  | ||||
|     this.employment.history && this.employment.history.sort( byDateDesc ); | ||||
|     this.education.history && this.education.history.sort( byDateDesc ); | ||||
|     this.service.history && this.service.history.sort( byDateDesc ); | ||||
|  | ||||
|     // this.awards && this.awards.sort( function(a, b) { | ||||
|     //   return( a.safeDate.isBefore(b.safeDate) ) ? 1 | ||||
|     //     : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0; | ||||
|     // }); | ||||
|     this.publications && this.publications.sort( function(a, b) { | ||||
|       return( a.safe.date.isBefore(b.safe.date) ) ? 1 | ||||
|         : ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0; | ||||
|     }); | ||||
|  | ||||
|     function byDateDesc(a,b) { | ||||
|       return( a.safe.start.isBefore(b.safe.start) ) ? 1 | ||||
|         : ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0; | ||||
|     } | ||||
|  | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|   Convert human-friendly dates into formal Moment.js dates for all collections. | ||||
|   We don't want to lose the raw textual date as entered by the user, so we store | ||||
|   the Moment-ified date as a separate property with a prefix of .safe. For ex: | ||||
|   job.startDate is the date as entered by the user. job.safeStartDate is the | ||||
|   parsed Moment.js date that we actually use in processing. | ||||
|   */ | ||||
|   function _parseDates() { | ||||
|  | ||||
|     var _fmt = require('./fluent-date').fmt; | ||||
|  | ||||
|     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 ) | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   Export the Sheet function/ctor. | ||||
|   */ | ||||
|   module.exports = FreshResume; | ||||
|  | ||||
| }()); | ||||
|  | ||||
| // Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats | ||||
| // in addition to YYYY-MM-DD. The original regex: | ||||
| // | ||||
| //     /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/ | ||||
| // | ||||
| @@ -1,6 +1,6 @@ | ||||
| /** | ||||
| Abstract character/resume sheet representation. | ||||
| @license Copyright (c) 2015 by James M. Devlin. All rights reserved. | ||||
| Definition of the JRSResume class. | ||||
| @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk | ||||
| */ | ||||
| 
 | ||||
| (function() { | ||||
| @@ -13,14 +13,14 @@ Abstract character/resume sheet representation. | ||||
|     , moment = require('moment'); | ||||
| 
 | ||||
|   /** | ||||
|   The Sheet class represent a specific JSON character sheet. When Sheet.open | ||||
|   The JRSResume class represent a specific JSON character sheet. When Sheet.open | ||||
|   is called, we merge the loaded JSON sheet properties onto the Sheet instance | ||||
|   via extend(), so a full-grown sheet object will have all of the methods here, | ||||
|   plus a complement of JSON properties from the backing JSON file. That allows | ||||
|   us to treat Sheet objects interchangeably with the loaded JSON model. | ||||
|   @class Sheet | ||||
|   @class JRSResume | ||||
|   */ | ||||
|   function Sheet() { | ||||
|   function JRSResume() { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
| @@ -29,18 +29,18 @@ Abstract character/resume sheet representation. | ||||
|   onto this Sheet instance with extend() and convert sheet dates to a safe & | ||||
|   consistent format. Then sort each section by startDate descending. | ||||
|   */ | ||||
|   Sheet.prototype.open = function( file, title ) { | ||||
|     this.meta = { fileName: file }; | ||||
|     this.meta.raw = FS.readFileSync( file, 'utf8' ); | ||||
|     return this.parse( this.meta.raw, title ); | ||||
|   JRSResume.prototype.open = function( file, title ) { | ||||
|     this.imp = { fileName: file }; | ||||
|     this.imp.raw = FS.readFileSync( file, 'utf8' ); | ||||
|     return this.parse( this.imp.raw, title ); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|   Save the sheet to disk (for environments that have disk access). | ||||
|   */ | ||||
|   Sheet.prototype.save = function( filename ) { | ||||
|     this.meta.fileName = filename || this.meta.fileName; | ||||
|     FS.writeFileSync( this.meta.fileName, this.stringify(), 'utf8' ); | ||||
|   JRSResume.prototype.save = function( filename ) { | ||||
|     this.imp.fileName = filename || this.imp.fileName; | ||||
|     FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' ); | ||||
|     return this; | ||||
|   }; | ||||
| 
 | ||||
| @@ -48,7 +48,7 @@ Abstract character/resume sheet representation. | ||||
|   Convert this object to a JSON string, sanitizing meta-properties along the | ||||
|   way. Don't override .toString(). | ||||
|   */ | ||||
|   Sheet.prototype.stringify = function() { | ||||
|   JRSResume.prototype.stringify = function() { | ||||
|     function replacer( key,value ) { // Exclude these keys from stringification
 | ||||
|       return _.some(['meta', 'warnings', 'computed', 'filt', 'ctrl', 'index', | ||||
|         'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', | ||||
| @@ -64,14 +64,14 @@ Abstract character/resume sheet representation. | ||||
|   onto this Sheet instance with extend() and convert sheet dates to a safe & | ||||
|   consistent format. Then sort each section by startDate descending. | ||||
|   */ | ||||
|   Sheet.prototype.parse = function( stringData, opts ) { | ||||
|   JRSResume.prototype.parse = function( stringData, opts ) { | ||||
|     opts = opts || { }; | ||||
|     var rep = JSON.parse( stringData ); | ||||
|     extend( true, this, rep ); | ||||
|     // Set up metadata
 | ||||
|     if( opts.meta === undefined || opts.meta ) { | ||||
|       this.meta = this.meta || { }; | ||||
|       this.meta.title = (opts.title || this.meta.title) || this.basics.name; | ||||
|     if( opts.imp === undefined || opts.imp ) { | ||||
|       this.imp = this.imp || { }; | ||||
|       this.imp.title = (opts.title || this.imp.title) || this.basics.name; | ||||
|     } | ||||
|     // Parse dates, sort dates, and calculate computed values
 | ||||
|     (opts.date === undefined || opts.date) && _parseDates.call( this ); | ||||
| @@ -86,7 +86,7 @@ Abstract character/resume sheet representation. | ||||
|   /** | ||||
|   Return a unique list of all keywords across all skills. | ||||
|   */ | ||||
|   Sheet.prototype.keywords = function() { | ||||
|   JRSResume.prototype.keywords = function() { | ||||
|     var flatSkills = []; | ||||
|     if( this.skills && this.skills.length ) { | ||||
|       this.skills.forEach( function( s ) { | ||||
| @@ -99,7 +99,7 @@ Abstract character/resume sheet representation. | ||||
|   /** | ||||
|   Update the sheet's raw data. TODO: remove/refactor | ||||
|   */ | ||||
|   Sheet.prototype.updateData = function( str ) { | ||||
|   JRSResume.prototype.updateData = function( str ) { | ||||
|     this.clear( false ); | ||||
|     this.parse( str ) | ||||
|     return this; | ||||
| @@ -108,9 +108,9 @@ Abstract character/resume sheet representation. | ||||
|   /** | ||||
|   Reset the sheet to an empty state. | ||||
|   */ | ||||
|   Sheet.prototype.clear = function( clearMeta ) { | ||||
|   JRSResume.prototype.clear = function( clearMeta ) { | ||||
|     clearMeta = ((clearMeta === undefined) && true) || clearMeta; | ||||
|     clearMeta && (delete this.meta); | ||||
|     clearMeta && (delete this.imp); | ||||
|     delete this.computed; // Don't use Object.keys() here
 | ||||
|     delete this.work; | ||||
|     delete this.volunteer; | ||||
| @@ -125,15 +125,15 @@ Abstract character/resume sheet representation. | ||||
|   /** | ||||
|   Get the default (empty) sheet. | ||||
|   */ | ||||
|   Sheet.default = function() { | ||||
|     return new Sheet().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); | ||||
|   JRSResume.default = function() { | ||||
|     return new JRSResume().open( PATH.join( __dirname, 'empty.json'), 'Empty' ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|   Add work experience to the sheet. | ||||
|   */ | ||||
|   Sheet.prototype.add = function( moniker ) { | ||||
|     var defSheet = Sheet.default(); | ||||
|   JRSResume.prototype.add = function( moniker ) { | ||||
|     var defSheet = JRSResume.default(); | ||||
|     var newObject = $.extend( true, {}, defSheet[ moniker ][0] ); | ||||
|     this[ moniker ] = this[ moniker ] || []; | ||||
|     this[ moniker ].push( newObject ); | ||||
| @@ -143,7 +143,7 @@ Abstract character/resume sheet representation. | ||||
|   /** | ||||
|   Determine if the sheet includes a specific social profile (eg, GitHub). | ||||
|   */ | ||||
|   Sheet.prototype.hasProfile = function( socialNetwork ) { | ||||
|   JRSResume.prototype.hasProfile = function( socialNetwork ) { | ||||
|     socialNetwork = socialNetwork.trim().toLowerCase(); | ||||
|     return this.basics.profiles && _.some( this.basics.profiles, function(p) { | ||||
|       return p.network.trim().toLowerCase() === socialNetwork; | ||||
| @@ -153,7 +153,7 @@ Abstract character/resume sheet representation. | ||||
|   /** | ||||
|   Determine if the sheet includes a specific skill. | ||||
|   */ | ||||
|   Sheet.prototype.hasSkill = function( skill ) { | ||||
|   JRSResume.prototype.hasSkill = function( skill ) { | ||||
|     skill = skill.trim().toLowerCase(); | ||||
|     return this.skills && _.some( this.skills, function(sk) { | ||||
|       return sk.keywords && _.some( sk.keywords, function(kw) { | ||||
| @@ -165,7 +165,7 @@ Abstract character/resume sheet representation. | ||||
|   /** | ||||
|   Validate the sheet against the JSON Resume schema. | ||||
|   */ | ||||
|   Sheet.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓
 | ||||
|   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') | ||||
| @@ -181,7 +181,7 @@ Abstract character/resume sheet representation. | ||||
|   *latest end date of all jobs in the work history*. This last condition is for | ||||
|   sheets that have overlapping jobs. | ||||
|   */ | ||||
|   Sheet.prototype.duration = function() { | ||||
|   JRSResume.prototype.duration = function() { | ||||
|     if( this.work && this.work.length ) { | ||||
|       var careerStart = this.work[ this.work.length - 1].safeStartDate; | ||||
|       if ((typeof careerStart === 'string' || careerStart instanceof String) && | ||||
| @@ -199,7 +199,7 @@ Abstract character/resume sheet representation. | ||||
|   Sort dated things on the sheet by start date descending. Assumes that dates | ||||
|   on the sheet have been processed with _parseDates(). | ||||
|   */ | ||||
|   Sheet.prototype.sort = function( ) { | ||||
|   JRSResume.prototype.sort = function( ) { | ||||
| 
 | ||||
|     this.work && this.work.sort( byDateDesc ); | ||||
|     this.education && this.education.sort( byDateDesc ); | ||||
| @@ -253,8 +253,8 @@ Abstract character/resume sheet representation. | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|   Export the Sheet function/ctor. | ||||
|   Export the JRSResume function/ctor. | ||||
|   */ | ||||
|   module.exports = Sheet; | ||||
|   module.exports = JRSResume; | ||||
| 
 | ||||
| }()); | ||||
							
								
								
									
										190
									
								
								src/fluentcmd.js
									
									
									
									
									
								
							
							
						
						
									
										190
									
								
								src/fluentcmd.js
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| /** | ||||
| Internal resume generation logic for FluentCV. | ||||
| @license Copyright (c) 2015 | James M. Devlin | ||||
| @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk | ||||
| @module fluentcmd.js | ||||
| */ | ||||
|  | ||||
| module.exports = function () { | ||||
| @@ -9,11 +10,12 @@ module.exports = function () { | ||||
|   var path = require( 'path' ) | ||||
|     , extend = require( './utils/extend' ) | ||||
|     , unused = require('./utils/string') | ||||
|     , fs = require('fs') | ||||
|     , FS = require('fs') | ||||
|     , _ = require('underscore') | ||||
|     , FLUENT = require('./fluentlib') | ||||
|     , PATH = require('path') | ||||
|     , MKDIRP = require('mkdirp') | ||||
|     //, COLORS = require('colors') | ||||
|     , rez, _log, _err; | ||||
|  | ||||
|   /** | ||||
| @@ -24,7 +26,7 @@ module.exports = function () { | ||||
|   @param theme Friendly name of the resume theme. Defaults to "modern". | ||||
|   @param logger Optional logging override. | ||||
|   */ | ||||
|   function gen( src, dst, opts, logger, errHandler ) { | ||||
|   function generate( src, dst, opts, logger, errHandler ) { | ||||
|  | ||||
|     _log = logger || console.log; | ||||
|     _err = errHandler || error; | ||||
| @@ -35,23 +37,20 @@ module.exports = function () { | ||||
|  | ||||
|     // Load input resumes... | ||||
|     if(!src || !src.length) { throw { fluenterror: 3 }; } | ||||
|     var sheets = src.map( function( res ) { | ||||
|       _log( 'Reading JSON resume: ' + res ); | ||||
|       return (new FLUENT.Sheet()).open( res ); | ||||
|     }); | ||||
|     var sheets = loadSourceResumes( src ); | ||||
|  | ||||
|     // Merge input resumes... | ||||
|     var msg = ''; | ||||
|     rez = _.reduceRight( sheets, function( a, b, idx ) { | ||||
|       msg += ((idx == sheets.length - 2) ? 'Merging ' + a.meta.fileName : '') | ||||
|         + ' onto ' + b.meta.fileName; | ||||
|       msg += ((idx == sheets.length - 2) ? 'Merging '.gray + a.imp.fileName : '') | ||||
|         + ' onto '.gray + b.imp.fileName; | ||||
|       return extend( true, b, a ); | ||||
|     }); | ||||
|     msg && _log(msg); | ||||
|  | ||||
|     // Load the active theme | ||||
|     // Verify the specified theme name/path | ||||
|     var tFolder = PATH.resolve( __dirname, '../node_modules/fluent-themes/themes', _opts.theme ); | ||||
|     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 ); | ||||
| @@ -59,18 +58,27 @@ module.exports = function () { | ||||
|         throw { fluenterror: 1, data: _opts.theme }; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Load the theme | ||||
|     var theTheme = new FLUENT.Theme().open( tFolder ); | ||||
|     _opts.themeObj = theTheme; | ||||
|     _log( 'Applying ' + theTheme.name.toUpperCase() + ' theme (' + Object.keys(theTheme.formats).length + ' formats)' ); | ||||
|     _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 = []; | ||||
|     var that = this; | ||||
|     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'; | ||||
|  | ||||
|       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.pre), fmt: z } }) | ||||
|         : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); | ||||
|         Object.keys( theTheme.formats ).map(function(k){ | ||||
|           var z = theTheme.formats[k]; | ||||
|           return { file: to.replace(/all$/g,z.pre), fmt: z } | ||||
|         }) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     // Run the transformation! | ||||
| @@ -87,14 +95,16 @@ module.exports = function () { | ||||
|   */ | ||||
|   function single( fi, theme ) { | ||||
|     try { | ||||
|       var f = fi.file, fType = fi.fmt.ext, fName = path.basename( f, '.' + fType ); | ||||
|       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 ); | ||||
|       _log( 'Generating ' + fi.fmt.title.toUpperCase() + ' resume: ' + path.relative(process.cwd(), f ) ); | ||||
|       var theFormat = _fmts.filter( function( fmt ) { | ||||
|         return fmt.name === fi.fmt.pre; | ||||
|       })[0]; | ||||
|       MKDIRP( path.dirname(fOut) ); // Ensure dest folder exists; don't bug user | ||||
|       var fOut = path.join( f.substring( 0, f.lastIndexOf('.')+1 ) + fObj.pre); | ||||
|  | ||||
|       _log( 'Generating '.useful + fi.fmt.title.toUpperCase().useful.bold + ' resume: '.useful + | ||||
|         path.relative(process.cwd(), f ).useful.bold ); | ||||
|  | ||||
|       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 ); | ||||
|     } | ||||
|     catch( ex ) { | ||||
| @@ -109,6 +119,128 @@ module.exports = function () { | ||||
|     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 ); | ||||
|         } | ||||
|         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 ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   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. | ||||
|   */ | ||||
| @@ -139,10 +271,18 @@ module.exports = function () { | ||||
|   Internal module interface. Used by FCV Desktop and HMR. | ||||
|   */ | ||||
|   return { | ||||
|     generate: gen, | ||||
|     verbs: { | ||||
|       build: generate, | ||||
|       validate: validate, | ||||
|       convert: convert, | ||||
|       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 | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| /** | ||||
| Core resume generation module for FluentCV. | ||||
| @license Copyright (c) 2015 by James M. Devlin. All rights reserved. | ||||
| External API surface for FluentCV:CLI. | ||||
| @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk | ||||
| */ | ||||
|  | ||||
| module.exports = { | ||||
|   Sheet: require('./core/sheet'), | ||||
|   Sheet: require('./core/fresh-resume'), | ||||
|   FRESHResume: require('./core/fresh-resume'), | ||||
|   JRSResume: require('./core/jrs-resume'), | ||||
|   Theme: require('./core/theme'), | ||||
|   FluentDate: require('./core/fluent-date'), | ||||
|   HtmlGenerator: require('./gen/html-generator'), | ||||
|   | ||||
| @@ -29,7 +29,9 @@ Base resume generator for FluentCV. | ||||
|       success: 0, | ||||
|       themeNotFound: 1, | ||||
|       copyCss: 2, | ||||
|       resumeNotFound: 3 | ||||
|       resumeNotFound: 3, | ||||
|       missingCommand: 4, | ||||
|       invalidCommand: 5 | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -19,9 +19,9 @@ var JsonGenerator = module.exports = BaseGenerator.extend({ | ||||
|   invoke: function( rez ) { | ||||
|     // TODO: merge with FCVD | ||||
|     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'], | ||||
|       'isModified', 'htmlPreview', 'safe' ], | ||||
|         function( val ) { return key.trim() === val; } | ||||
|       ) ? undefined : value; | ||||
|     } | ||||
|   | ||||
| @@ -31,7 +31,9 @@ var _defaultOpts = { | ||||
|     xml: function( txt ) { return XML(txt); }, | ||||
|     md: function( txt ) { return MD(txt); }, | ||||
|     mdin: function( txt ) { return MD(txt).replace(/^\s*\<p\>|\<\/p\>\s*$/gi, ''); }, | ||||
|     lower: function( txt ) { return txt.toLowerCase(); } | ||||
|     lower: function( txt ) { return txt.toLowerCase(); }, | ||||
|     link: function( name, url ) { return url ? | ||||
|       '<a href="' + url + '">' + name + '</a>' : name } | ||||
|   }, | ||||
|   prettify: { // ← See https://github.com/beautify-web/js-beautify#options | ||||
|     indent_size: 2, | ||||
| @@ -125,7 +127,6 @@ var TemplateGenerator = module.exports = BaseGenerator.extend({ | ||||
|  | ||||
|     // Strip {# comments #} | ||||
|     jst = jst.replace( _.templateSettings.comment, ''); | ||||
|     json.display_progress_bar = true; | ||||
|  | ||||
|     // Compile and run the template. TODO: avoid unnecessary recompiles. | ||||
|     jst = _.template(jst)({ r: json, filt: this.opts.filters, cssInfo: cssInfo, headFragment: this.opts.headFragment || '' }); | ||||
|   | ||||
							
								
								
									
										88
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								src/index.js
									
									
									
									
									
								
							| @@ -1,14 +1,19 @@ | ||||
| #! /usr/bin/env node | ||||
|  | ||||
| /** | ||||
| Command-line interface (CLI) for FluentCV via Node.js. | ||||
| @license Copyright (c) 2015 | James M. Devlin | ||||
| Command-line interface (CLI) for FluentCV:CLI. | ||||
| @license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk. | ||||
| */ | ||||
|  | ||||
| var ARGS = require( 'minimist' ) | ||||
|   , FCMD  = require( './fluentcmd') | ||||
|   , PKG = require('../package.json') | ||||
|   , opts = { }; | ||||
|   , COLORS = require('colors') | ||||
|   , FS = require('fs') | ||||
|   , PATH = require('path') | ||||
|   , opts = { } | ||||
|   , title = ('*** FluentCV v' + PKG.version + ' ***').bold.white | ||||
|   , _ = require('underscore'); | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -23,20 +28,53 @@ catch( ex ) { | ||||
|  | ||||
| function main() { | ||||
|  | ||||
|   // Setup. | ||||
|   var title = '*** FluentCV v' + PKG.version + ' ***'; | ||||
|   if( process.argv.length <= 2 ) { logMsg(title); throw { fluenterror: 3 }; } | ||||
|   var args = ARGS( process.argv.slice(2) ); | ||||
|   opts = getOpts( args ); | ||||
|   // Colorize | ||||
|   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', | ||||
|   }); | ||||
|  | ||||
|   // Setup | ||||
|   if( process.argv.length <= 2 ) { throw { fluenterror: 4 }; } | ||||
|   var a = ARGS( process.argv.slice(2) ); | ||||
|   opts = getOpts( a ); | ||||
|   logMsg( title ); | ||||
|  | ||||
|   // Convert arguments to source files, target files, options | ||||
|   var src = args._ || []; | ||||
|   var dst = (args.o && ((typeof args.o === 'string' && [ args.o ]) || args.o)) || []; | ||||
|   dst = (dst === true) ? [] : dst; // Handle -o with missing output file | ||||
|  | ||||
|   // Generate! | ||||
|   FCMD.generate( src, dst, opts, logMsg ); | ||||
|   // Get the action to be performed | ||||
|   var params = a._.map( function(p){ return p.toLowerCase().trim(); }); | ||||
|   var verb = params[0]; | ||||
|   if( !FCMD.verbs[ verb ] ) { | ||||
|     logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Get source and dest params | ||||
|   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 ); | ||||
|     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 | ||||
|   FCMD.verbs[ verb ].apply( null, parms ); | ||||
|  | ||||
| } | ||||
|  | ||||
| function logMsg( msg ) { | ||||
| @@ -55,13 +93,27 @@ function getOpts( args ) { | ||||
|  | ||||
| function handleError( ex ) { | ||||
|   var msg = '', exitCode; | ||||
|  | ||||
|  | ||||
|  | ||||
|   if( ex.fluenterror ){ | ||||
|     switch( ex.fluenterror ) { // TODO: Remove magic numbers | ||||
|       case 1: msg = "The specified theme couldn't be found: " + ex.data; break; | ||||
|       case 2: msg = "Couldn't copy CSS file to destination folder"; break; | ||||
|       case 3: msg = "Please specify a valid JSON resume file."; break; | ||||
|       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; | ||||
|         }).join(', '.guide) + ") to get started.\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; | ||||
|  | ||||
|   } | ||||
|   else { | ||||
|     msg = ex.toString(); | ||||
| @@ -70,7 +122,11 @@ function handleError( ex ) { | ||||
|  | ||||
|   var idx = msg.indexOf('Error: '); | ||||
|   var trimmed = idx === -1 ? msg : msg.substring( idx + 7 ); | ||||
|   console.log( 'ERROR: ' + trimmed.toString() ); | ||||
|   if( !ex.fluenterror || ex.fluenterror < 3 ) | ||||
|     console.log( ('ERROR: ' + trimmed.toString()).red.bold ); | ||||
|   else | ||||
|     console.log( trimmed.toString() ); | ||||
|  | ||||
|   process.exit( exitCode ); | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										22
									
								
								src/use.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/use.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| Usage: | ||||
|  | ||||
|     fluentcv <COMMAND> <SOURCES> [TO <TARGETS>] [-t <THEME>] | ||||
|  | ||||
| <COMMAND> should be BUILD, CONVERT, VALIDATE, or HELP. <SOURCES> should | ||||
| be the path to one or more FRESH or JSON Resume format resumes. <TARGETS> | ||||
| should be the name of the destination resume to be created, if any. The | ||||
| <THEME> 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. | ||||
|  | ||||
|     fluentcv BUILD resume.json TO out/resume.all | ||||
|     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 VALIDATE resume.json resume2.json resume3.json | ||||
|  | ||||
| See https://github.com/fluentdesk/fluentCV/blob/master/README.md | ||||
| for more information. | ||||
							
								
								
									
										130
									
								
								tests/jrs-exemplar/richard-hendriks.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								tests/jrs-exemplar/richard-hendriks.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| { | ||||
|   "basics": { | ||||
|     "name": "Richard Hendriks", | ||||
|     "label": "Programmer", | ||||
|     "picture": "", | ||||
|     "email": "richard.hendriks@gmail.com", | ||||
|     "phone": "(912) 555-4321", | ||||
|     "website": "http://richardhendricks.com", | ||||
|     "summary": "Richard hails from Tulsa. He has earned degrees from the University of Oklahoma and Stanford. (Go Sooners and Cardinals!) Before starting Pied Piper, he worked for Hooli as a part time software developer. While his work focuses on applied information theory, mostly optimizing lossless compression schema of both the length-limited and adaptive variants, his non-work interests range widely, everything from quantum computing to chaos theory. He could tell you about it, but THAT would NOT be a “length-limited” conversation!", | ||||
|     "location": { | ||||
|       "address": "2712 Broadway St", | ||||
|       "postalCode": "CA 94115", | ||||
|       "city": "San Francisco", | ||||
|       "countryCode": "US", | ||||
|       "region": "California" | ||||
|     }, | ||||
|     "profiles": [ | ||||
|       { | ||||
|         "network": "Twitter", | ||||
|         "username": "neutralthoughts", | ||||
|         "url": "" | ||||
|       }, | ||||
|       { | ||||
|         "network": "SoundCloud", | ||||
|         "username": "dandymusicnl", | ||||
|         "url": "https://soundcloud.com/dandymusicnl" | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "work": [ | ||||
|     { | ||||
|       "company": "Pied Piper", | ||||
|       "position": "CEO/President", | ||||
|       "website": "http://piedpiper.com", | ||||
|       "startDate": "2013-12-01", | ||||
|       "endDate": "2014-12-01", | ||||
|       "summary": "Pied Piper is a multi-platform technology based on a proprietary universal compression algorithm that has consistently fielded high Weisman Scores™ that are not merely competitive, but approach the theoretical limit of lossless compression.", | ||||
|       "highlights": [ | ||||
|         "Build an algorithm for artist to detect if their music was violating copy right infringement laws", | ||||
|         "Successfully won Techcrunch Disrupt", | ||||
|         "Optimized an algorithm that holds the current world record for Weisman Scores" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "volunteer": [ | ||||
|     { | ||||
|       "organization": "CoderDojo", | ||||
|       "position": "Teacher", | ||||
|       "website": "http://coderdojo.com/", | ||||
|       "startDate": "2012-01-01", | ||||
|       "endDate": "2013-01-01", | ||||
|       "summary": "Global movement of free coding clubs for young people.", | ||||
|       "highlights": [ | ||||
|         "Awarded 'Teacher of the Month'" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "education": [ | ||||
|     { | ||||
|       "institution": "University of Oklahoma", | ||||
|       "area": "Information Technology", | ||||
|       "studyType": "Bachelor", | ||||
|       "startDate": "2011-06-01", | ||||
|       "endDate": "2014-01-01", | ||||
|       "gpa": "4.0", | ||||
|       "courses": [ | ||||
|         "DB1101 - Basic SQL", | ||||
|         "CS2011 - Java Introduction" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "awards": [ | ||||
|     { | ||||
|       "title": "Digital Compression Pioneer Award", | ||||
|       "date": "2014-11-01", | ||||
|       "awarder": "Techcrunch", | ||||
|       "summary": "There is no spoon." | ||||
|     } | ||||
|   ], | ||||
|   "publications": [ | ||||
|     { | ||||
|       "name": "Video compression for 3d media", | ||||
|       "publisher": "Hooli", | ||||
|       "releaseDate": "2014-10-01", | ||||
|       "website": "http://en.wikipedia.org/wiki/Silicon_Valley_(TV_series)", | ||||
|       "summary": "Innovative middle-out compression algorithm that changes the way we store data." | ||||
|     } | ||||
|   ], | ||||
|   "skills": [ | ||||
|     { | ||||
|       "name": "Web Development", | ||||
|       "level": "Master", | ||||
|       "keywords": [ | ||||
|         "HTML", | ||||
|         "CSS", | ||||
|         "Javascript" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "name": "Compression", | ||||
|       "level": "Master", | ||||
|       "keywords": [ | ||||
|         "Mpeg", | ||||
|         "MP4", | ||||
|         "GIF" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "languages": [ | ||||
|     { | ||||
|       "language": "English", | ||||
|       "fluency": "Native speaker" | ||||
|     } | ||||
|   ], | ||||
|   "interests": [ | ||||
|     { | ||||
|       "name": "Wildlife", | ||||
|       "keywords": [ | ||||
|         "Ferrets", | ||||
|         "Unicorns" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "references": [ | ||||
|     { | ||||
|       "name": "Erlich Bachman", | ||||
|       "reference": "It is my pleasure to recommend Richard, his performance working as a consultant for Main St. Company proved that he will be a valuable addition to any company." | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										36
									
								
								tests/test-converter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								tests/test-converter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
|  | ||||
| var chai = require('chai') | ||||
|   , expect = chai.expect | ||||
|   , should = chai.should() | ||||
|   , path = require('path') | ||||
|   , _ = require('underscore') | ||||
| 	, FRESHResume = require('../src/core/fresh-resume') | ||||
|   , CONVERTER = require('../src/core/convert') | ||||
|   , FS = require('fs') | ||||
|   , _ = require('underscore'); | ||||
|  | ||||
| chai.config.includeStack = false; | ||||
|  | ||||
| describe('FRESH/JRS converter', function () { | ||||
|  | ||||
|     var _sheet; | ||||
|  | ||||
| 	  it('should round-trip from JRS to FRESH to JRS without modifying or losing data', function () { | ||||
|  | ||||
|       var fileA = path.join( __dirname, 'jrs-exemplar/richard-hendriks.json' ); | ||||
|       var fileB =  path.join( __dirname, 'sandbox/richard-hendriks.json' ); | ||||
|  | ||||
|       _sheet = new FRESHResume().open( fileA ); | ||||
|       _sheet.saveAs( fileB, 'JRS' ); | ||||
|  | ||||
|       var rawA = FS.readFileSync( fileA, 'utf8' ); | ||||
|       var rawB = FS.readFileSync( fileB, 'utf8' ); | ||||
|  | ||||
|       var objA = JSON.parse( rawA ); | ||||
|       var objB = JSON.parse( rawB ); | ||||
|  | ||||
|       _.isEqual(objA, objB).should.equal(true); | ||||
|  | ||||
|     }); | ||||
|  | ||||
| }); | ||||
							
								
								
									
										73
									
								
								tests/test-fresh-sheet.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								tests/test-fresh-sheet.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
|  | ||||
| var chai = require('chai') | ||||
|   , expect = chai.expect | ||||
|   , should = chai.should() | ||||
|   , path = require('path') | ||||
|   , _ = require('underscore') | ||||
| 	, FRESHResume = require('../src/core/fresh-resume') | ||||
|   , validator = require('is-my-json-valid'); | ||||
|  | ||||
| chai.config.includeStack = false; | ||||
|  | ||||
| describe('jane-doe.json (FRESH)', function () { | ||||
|  | ||||
|     var _sheet; | ||||
|  | ||||
| 	  it('should open without throwing an exception', function () { | ||||
|       function tryOpen() { | ||||
|         _sheet = new FRESHResume().open( | ||||
|           'node_modules/FRESCA/exemplar/jane-doe.json' ); | ||||
|       } | ||||
|       tryOpen.should.not.Throw(); | ||||
|     }); | ||||
|  | ||||
|     it('should have one or more of each section', function() { | ||||
|       expect( | ||||
|         //(_sheet.basics) && | ||||
|         _sheet.name && _sheet.info && _sheet.location && _sheet.contact && | ||||
|         (_sheet.employment.history && _sheet.employment.history.length > 0) && | ||||
|         (_sheet.skills && _sheet.skills.list.length > 0) && | ||||
|         (_sheet.education.history && _sheet.education.history.length > 0) && | ||||
|         (_sheet.service.history && _sheet.service.history.length > 0) && | ||||
|         (_sheet.writing && _sheet.writing.length > 0) && | ||||
|         (_sheet.recognition && _sheet.recognition.length > 0) && | ||||
|         (_sheet.samples && _sheet.samples.length > 0) && | ||||
|         (_sheet.references && _sheet.references.length > 0) && | ||||
|         (_sheet.interests && _sheet.interests.length > 0) | ||||
|       ).to.equal( true ); | ||||
|     }); | ||||
|  | ||||
|     it('should have a work duration of 7 years', function() { | ||||
|       _sheet.computed.numYears.should.equal( 7 ); | ||||
|     }); | ||||
|  | ||||
|     it('should save without throwing an exception', function(){ | ||||
|       function trySave() { | ||||
|         _sheet.save( 'tests/sandbox/jane-doe.json' ); | ||||
|       } | ||||
|       trySave.should.not.Throw(); | ||||
|     }); | ||||
|  | ||||
|     it('should not be modified after saving', function() { | ||||
|       var savedSheet = new FRESHResume().open('tests/sandbox/jane-doe.json'); | ||||
|       _sheet.stringify().should.equal( savedSheet.stringify() ) | ||||
|     }); | ||||
|  | ||||
|     it('should validate against the FRESH resume schema', function() { | ||||
|       var result = _sheet.isValid(); | ||||
|       // var schemaJson = require('FRESCA'); | ||||
|       // var validate = validator( schemaJson, { verbose: true } ); | ||||
|       // var result = validate( JSON.parse( _sheet.imp.raw ) ); | ||||
|       result || console.log("\n\nOops, resume didn't validate. " + | ||||
|         "Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n"); | ||||
|       result.should.equal( true ); | ||||
|     }); | ||||
|  | ||||
|  | ||||
| }); | ||||
|  | ||||
| // describe('subtract', function () { | ||||
| // 	it('should return -1 when passed the params (1, 2)', function () { | ||||
| // 		expect(math.subtract(1, 2)).to.equal(-1); | ||||
| // 	}); | ||||
| // }); | ||||
| @@ -4,18 +4,18 @@ var chai = require('chai') | ||||
|   , should = chai.should() | ||||
|   , path = require('path') | ||||
|   , _ = require('underscore') | ||||
| 	, Sheet = require('../src/core/sheet') | ||||
| 	, JRSResume = require('../src/core/jrs-resume') | ||||
|   , validator = require('is-my-json-valid'); | ||||
| 
 | ||||
| chai.config.includeStack = false; | ||||
| 
 | ||||
| describe('fullstack.json', function () { | ||||
| describe('fullstack.json (JRS)', function () { | ||||
| 
 | ||||
|     var _sheet; | ||||
| 
 | ||||
| 	  it('should open without throwing an exception', function () { | ||||
|       function tryOpen() { | ||||
|         _sheet = new Sheet().open( 'node_modules/resample/fullstack/in/resume.json' ); | ||||
|         _sheet = new JRSResume().open( 'node_modules/resample/fullstack/in/resume.json' ); | ||||
|       } | ||||
|       tryOpen.should.not.Throw(); | ||||
|     }); | ||||
| @@ -44,14 +44,14 @@ describe('fullstack.json', function () { | ||||
|     }); | ||||
| 
 | ||||
|     it('should not be modified after saving', function() { | ||||
|       var savedSheet = new Sheet().open( 'tests/sandbox/fullstack.json' ); | ||||
|       var savedSheet = new JRSResume().open( 'tests/sandbox/fullstack.json' ); | ||||
|       _sheet.stringify().should.equal( savedSheet.stringify() ) | ||||
|     }); | ||||
| 
 | ||||
|     it('should validate against the JSON Resume schema', function() { | ||||
|       var schemaJson = require('../src/core/resume.json'); | ||||
|       var validate = validator( schemaJson, { verbose: true } ); | ||||
|       var result = validate( JSON.parse( _sheet.meta.raw ) ); | ||||
|       var result = validate( JSON.parse( _sheet.imp.raw ) ); | ||||
|       result || console.log("\n\nOops, resume didn't validate. " + | ||||
|        "Validation errors:\n\n", validate.errors, "\n\n"); | ||||
|       result.should.equal( true ); | ||||
		Reference in New Issue
	
	Block a user