1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2025-05-12 00:27:08 +01:00

Compare commits

...

83 Commits

Author SHA1 Message Date
901659cc4f make package depend on my upstream theme 2020-03-17 04:20:38 +00:00
3cf850ea0e Update test exemplars. 2016-02-14 05:32:33 -05:00
1b0bc87b60 Update changelog and version. 2016-02-14 04:54:44 -05:00
5d3b993737 Bump fresh-themes to 0.15.1-beta.
Not 100% necessary given "^" but support naive stripping of the "^"
decorator.
2016-02-14 04:32:39 -05:00
917fd8e3f3 Refactor helpers.
Rebind Handlebars helpers to drop the pesky options hash for standalone
helpers that don't need it. Move block helpers (which do need the
Handlebars options/context) to a separate file for special handling.
2016-02-14 04:10:23 -05:00
6ac2cd490b Bump fresh-test-resumes to 0.7.0. 2016-02-13 23:45:53 -05:00
8100190978 Bump fresh-themes to 0.15.0-beta. 2016-02-13 22:54:43 -05:00
7c36ff8331 Introduce "date" helper. 2016-02-13 22:54:07 -05:00
255a518565 Set test timeout to 30 seconds.
Most themes should generate in < 1s but allow up to 30 seconds for
network latency when opening a remote file, or for fetching remote
resources (CSS, JS, etc) during a local build.
2016-02-13 20:42:03 -05:00
2d595350c6 Escape LaTeX during generation. 2016-02-13 20:40:17 -05:00
ca92d41d9e Numerous fixes. 2016-02-13 16:08:45 -05:00
3f8e795c61 Fix generation glitches.
Fix output file name glitch, writing CSS files to destination folder,
and an issue where the process would evaporate before PDF/PNG generation
could complete.
2016-02-13 03:27:11 -05:00
9927e79900 Clean up CoffeeScript. 2016-02-13 00:40:10 -05:00
dbef9f0a35 Improve VALIDATE error handling. 2016-02-13 00:11:52 -05:00
c889664c31 More VALIDATE fixups. 2016-02-12 23:47:08 -05:00
7a60cd0bab Fixup VALIDATE command.
Introduce MISSING and UNKNOWN states alongside BROKEN, VALID, and
INVALID and fix regressions introduced in previous refactorings.
2016-02-12 22:49:56 -05:00
964350d3c7 Bump fresh-jrs-converter to 0.2.2.
Technically the "^0.2.1" implies v0.2.2 but eventually we'll drop the
"^" and use shrinkwrapped versions in dev, so explicitly bump for now.
2016-02-12 21:42:50 -05:00
b57d9f05af jsHint: Allow == null.
Relax jsHint's barbaric and antiquated default behavior on triple-equals
null comparisons. Allow expressive, precise, and subtle expressions such
as CoffeeScript's use of "== null" for testing against null OR undefined
under the existential operator.
2016-02-12 17:42:48 -05:00
b26799f9fc Improve JSON error handling.
Add support for detection of invalid line breaks in JSON string values.
Fixes #137. Could be improved to fetch the column number and drop the
messy grabbing of the line number from the exception message via regex,
but currently the "jsonlint" library (not to be confused with
"json-lint") only emits an error string. Since this is also the library
that drives http://jsonlint.com, we'll accept the messy regex in return
for more robust error checking when our default json-lint path fails.

All of the above only necessary because standard JSON.parse error
handling is broken in all environments. : )
2016-02-12 17:11:11 -05:00
daeffd27b5 Remove HB reference from generic helpers. 2016-02-11 22:06:43 -05:00
f87eb46549 Fix theme generation error. 2016-02-11 22:04:11 -05:00
da7cd28734 Remove unused var. 2016-02-11 22:03:49 -05:00
31e0bb69cc Introduce "pad()" helper.
Introduce a helper to emit padded strings / arrays of strings.
2016-02-11 22:02:50 -05:00
5c248cca2a Remove output folder. 2016-02-11 12:09:47 -05:00
f83eb018e8 Scrub tests. 2016-02-11 12:08:11 -05:00
317a250917 Gather. 2016-02-11 11:48:44 -05:00
aaa5e1fc1f Refactor generation.
Merge implicit and explicit generation paths, start emitting file
transform & copy signals, fix various bugs, introduce new bugs, support
better --debug outputs in the future.
2016-02-09 15:27:34 -05:00
1bc4263a46 Aerate. 2016-02-09 10:50:10 -05:00
e191af1fb0 Fix glitch in converted CoffeeScript.
Replace naked ternary with if then else.
2016-02-09 10:41:48 -05:00
7c0a9bcc02 Aerate. 2016-02-09 10:37:33 -05:00
d894f62607 Add ResumeFactory to facade.
Until facade is decommissioned and mothballed
2016-02-09 08:55:00 -05:00
2758038858 Cleanup and bug fixes.
Remove file-based open methods from resume classes; force clients to use
clean string-based or JSON overloads; fix processing glitch in
validate(); tweak outputs; adjust tests; update CHANGELOG; etc.
2016-02-04 18:49:16 -05:00
661fb91861 Aerate. 2016-02-04 15:23:47 -05:00
3c551eb923 Point package.json "main" at "dist" folder. 2016-02-04 14:38:11 -05:00
5bf4bda6de Fix PEEK command. 2016-02-03 20:08:17 -05:00
49ae016f08 Deglitch. 2016-02-02 19:02:56 -05:00
89957aed76 Scrub.
Adding slightly heavier function-level comments as a start for API docs.
2016-02-02 17:47:32 -05:00
233025ddcc Fix indentation. 2016-02-02 17:46:38 -05:00
11dd8952d8 Improve PEEK behavior. 2016-02-02 17:34:10 -05:00
d7c83613df Make CLI tests asynchronous. 2016-02-02 16:18:38 -05:00
a456093f13 Clean up a couple regressions. 2016-02-02 14:13:38 -05:00
dd4851498a Remove Resig's class implementation.
Fun while it lasted.
2016-02-02 13:49:02 -05:00
f72b02a0f4 Refactor generators to CoffeeScript classes. 2016-02-02 13:38:12 -05:00
63a0c78fc5 Refactor verbs to CoffeeScript classes.
Retire Resig's class implementation.
2016-02-01 23:16:49 -05:00
fd39cc9fd9 Adjust error handling / tests. 2016-02-01 22:56:08 -05:00
70f45d468d Asynchrony. 2016-02-01 22:52:13 -05:00
212b01092c Improve proc spawn behavior.
Interim until async / promises support is in.
2016-02-01 09:25:22 -05:00
36d641801b Add Gitter chat badge. 2016-01-31 20:02:27 -05:00
bd278268f6 Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-31 12:21:44 -05:00
abe31e30e0 Update license year range to 2016 2016-01-31 12:21:29 -05:00
314d8d8763 Introduce build instructions. 2016-01-31 12:17:17 -05:00
ed0792e8f8 Fix YML/JSON/PNG invalid output format warning.
Fixes #97 but we still need to support standalone PNG (ie, a PNG not
generated as part of a .all output target).
2016-01-31 09:41:00 -05:00
90765bf90b Refactor verb invocations to base. 2016-01-31 08:37:12 -05:00
f1ba7765ee Include date tests. 2016-01-30 20:20:32 -05:00
27c7a0264a Improve date handling. 2016-01-30 20:06:04 -05:00
8e806dc04f Improve duration calcs, intro base resume class. 2016-01-30 16:40:22 -05:00
8ec6b5ed6a Bump version to 1.7.4. 2016-01-30 12:08:02 -05:00
4ef4ec5d42 Remove Node. 4.5.
Travis support 4.1 and 5.0 but not 4.5.
2016-01-30 11:49:27 -05:00
2f523b845b Travis: Add Node 4.5. 2016-01-30 11:40:36 -05:00
1c416f39d3 Fix JSON Resume theme breakage.
Fixes #128.
2016-01-30 11:31:39 -05:00
1de0eff7b3 Merge pull request #114 from pra85/patch-1
Update license year range to 2016
2016-01-29 22:32:45 -05:00
f8a39b0908 Update license year range to 2016 2016-01-30 07:41:15 +05:30
d69e4635be Bump fresh-themes to 0.14.1-beta. 2016-01-29 16:14:53 -05:00
4b7d594502 Bump version to 1.7.3. 2016-01-29 15:50:34 -05:00
896b7055c1 Fix issue with undefined sections.
Fixes #127.
2016-01-29 15:50:21 -05:00
0f65e4c9f3 Finish HackMyCore reshaping.
Reintroduce HackMyCore, dropping the interim submodule, and reorganize
and improve tests.
2016-01-29 15:23:57 -05:00
e9971eb882 Bump version to 1.7.2. 2016-01-28 07:05:27 -05:00
beb60d4074 Integrate HMC. 2016-01-27 05:29:26 -05:00
4440d23584 Move HackMyCore submodule to /src. 2016-01-27 04:33:45 -05:00
aca67cec29 Add HMC as a submodule! 2016-01-27 04:22:41 -05:00
75a953aa73 Bump HackMyCore to 0.4.0. 2016-01-26 14:44:08 -05:00
15a0af8410 Fix output glitches. 2016-01-26 14:43:48 -05:00
9f811336e4 Bump HackMyCore version to 0.3.0. 2016-01-26 13:18:17 -05:00
a07faf6d50 ... 2016-01-26 11:43:49 -05:00
f098ed507f ... 2016-01-26 11:39:24 -05:00
80c36b96bc ... 2016-01-26 10:58:10 -05:00
630cf59cfb Caffeinate. 2016-01-26 06:59:34 -05:00
165eb5d9cd Remove extraneous console.log added by Calhoun. 2016-01-25 20:57:21 -05:00
d12e970af5 Exclude files from NPM. 2016-01-25 12:06:40 -05:00
cf18c5d90d Tweak test & clean. 2016-01-25 10:55:25 -05:00
0497696dcf Bump version to 1.7.1. 2016-01-25 10:41:47 -05:00
d007bd9bf6 Introduce CoffeeScript and build step. 2016-01-25 10:34:57 -05:00
5838b085c7 Fix console helpers path. 2016-01-24 18:51:08 -05:00
159 changed files with 20098 additions and 1016 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ doc/
docs/
local/
npm-debug.log
*.map
# Emacs detritus
# -*- mode: gitignore; -*-

7
.npmignore Normal file
View File

@ -0,0 +1,7 @@
src/
assets/
test/
doc/
.travis.yml
Gruntfile.js
.gitattributes

58
BUILDING.md Normal file
View File

@ -0,0 +1,58 @@
Building
========
*See [CONTRIBUTING.md][contrib] for more information on contributing to the
HackMyResume or FluentCV projects.*
HackMyResume is a standard Node.js command line app implemented in a mix of
CoffeeScript and JavaScript. Setting up a build environment is easy:
## Prerequisites ##
1. OS: Linux, OS X, or Windows
2. Install [Node.js][node] and [Grunt][grunt].
## Set up a build environment ###
1. Fork [hacksalot/HackMyResume][hmr] to your GitHub account.
2. Clone your fork locally.
3. From within the top-level HackMyResume folder, run `npm install` to install
project dependencies.
4. Create a new branch, based on the latest HackMyResume `dev` branch, to
contain your work.
5. Run `npm link` in the HackMyResume folder so that the `hackmyresume` command
will reference your local installation (you may need to
`npm uninstall -g hackmyresume` first).
## Making changes
1. HackMyResume sources live in the [`/src`][src] folder. Always make your edits
there, never in the generated `/dist` folder.
2. After making your changes, run `grunt build` to package the HackMyResume
sources to the `/dist` folder. This will transform CoffeeScript files to
JavaScript and perform other build steps as necessary. In the future, a watch
task or guardfile will be added to automate this step.
3. Do local spot testing with `hackmyresume` as normal.
4. When you're ready to submit your changes, run `grunt test` to run the HMR
test suite. Fix any errors that occur.
5. Commit and push your changes.
6. Submit a pull request targeting the HackMyResume `dev` branch.
[node]: https://nodejs.org/en/
[grunt]: http://gruntjs.com/
[hmr]: https://github.com/hacksalot/HackMyResume
[src]: https://github.com/hacksalot/HackMyResume/tree/master/src
[contrib]: https://github.com/hacksalot/HackMyResume/blob/master/CONTRIBUTING.md

View File

@ -1,5 +1,108 @@
CHANGELOG
=========
## v1.8.0
### Added
- Updated `Awesome` theme to latest version of [Awesome-CV][acv].
- Introduced new theme helpers: `pad`, `date`.
### Fixed
- Fixed an issue where the `Awesome` theme wouldn't correctly generate LaTeX
outputs (#138).
- Emit a line number for syntax errors around embedded newlines in JSON strings
(#137).
- Fix several PDF / PNG generation errors (#132, others).
- Display a more helpful error message when attempting to generate a PDF or PNG
on a machine where PhantomJS and/or wkhtmltopdf are either not installed or
not path-accessible.
- Fixed an issue that would cause long-running PDF/PNG generation to fail in
certain environments.
- Fixed an issue involving an unhelpful spawn-related exception (#136).
### Internal
- JSHint will no longer gripe at the use of `== null` and `!= null` in
CoffeeScript transpilation.
- Introduced [template-friendly Awesome-CV fork][awefork] to isolate template
expansion logic & provide better durability for HackMyResume's `awesome` theme.
- Fixed a couple temporary regressions (#139, #140) on the dev branch.
- Additional tests.
- Minor breaking HackMyResume API changes.
## v1.7.4
### Added
- [Build instructions](https://github.com/hacksalot/HackMyResume/blob/master/BUILDING.md).
### Changed
- More precise date handling.
### Fixed
- Issue with incomplete PDF generation (#127).
- Issue with building JSON Resume themes (#128).
- Issue with generating `.json` output format by itself (#97).
## v1.7.3
### Fixed
- Issue with generated PDFs being chopped off and displaying a mysterious sequence of numbers of unknown and possibly alien origin (#127).
- Unsightly border on Modern:PDF.
- Modern|Positive:PDF formats now correctly reference their PDF-specific CSS files.
- `Incorrect helper use` warning in Positive:DOC.
## v1.7.2
### Changed
- Interim release supporting FluentCV Desktop.
### Internal
- Moved [HackMyCore](https://github.com/hacksalot/HackMyCore) dependency to
submodule.
## v1.7.1
### Changed
- Caffeinate. CoffeeScript now used throughout
[HackMyResume](https://github.com/hacksalot/HackMyResume) and
[HackMyCore](https://github.com/hacksalot/HackMyCore); generated JavaScript
lives in `/dist`.
### Fixed
- Issue with generating a single PDF with the `.pdf` extension (#99).
## v1.7.0
### Changed
- [Internal] Relocated HMR processing code to the
[HackMyCore](https://github.com/hacksalot/HackMyCore) project. Shouldn't affect
normal use.
## v1.6.0
### Major Improvements
@ -304,3 +407,5 @@ theme.
[i111]: https://github.com/hacksalot/HackMyResume/issues/111
[fresca]: https://github.com/fluentdesk/FRESCA
[themes]: https://github.com/fluentdesk/fresh-themes
[awefork]: https://github.com/fluentdesk/Awesome-CV
[acv]: https://github.com/posquit0/Awesome-CV

View File

@ -4,17 +4,11 @@ Contributing
*Note: HackMyResume is also available as [FluentCV][fcv]. Contributors are
credited in both.*
HackMyResume needs your help! Our contribution workflow is based on [GitHub
Flow][flow] and we respond to all pull requests and issues, usually within 24
hours. HackMyResume has no corporate affiliation and no commercial basis, which
allows the project to maintain a strict user-first policy, rapid development
velocity, and a liberal stance on contributions and exotic functionality in
keeping with the spirit (and name) of the tool.
In short, your code is welcome here.
## How To Contribute
*See [BUILDING.md][building] for instructions on setting up a HackMyResume
development environment.*
1. Optional: [**open an issue**][iss] identifying the feature or bug you'd like
to implement or fix. This step isn't required — you can start hacking away on
HackMyResume without clearing it with us — but helps avoid duplication of work
@ -25,7 +19,7 @@ similar; call it whatever you like) to perform your work in.
4. **Install dependencies** by running `npm install` in the top-level
HackMyResume folder.
5. Make your **commits** as usual.
6. **Verify** your changes locally with `npm test`.
6. **Verify** your changes locally with `grunt test`.
7. **Push** your commits.
7. **Submit a pull request** from your feature branch to the HackMyResume `dev`
branch.
@ -48,7 +42,7 @@ You can reach hacksalot directly at:
hacksalot@indevious.com
```
Thanks! See you out there in the trenches.
Thanks for your interest in the HackMyResume project.
[fcv]: https://github.com/fluentdesk/fluentcv
[flow]: https://guides.github.com/introduction/flow/
@ -56,3 +50,4 @@ Thanks! See you out there in the trenches.
[ha]: https://github.com/hacksalot
[th]: https://github.com/tomheon
[awesome]: https://github.com/hacksalot/HackMyResume/graphs/contributors
[building]: https://github.com/hacksalot/HackMyResume/blob/master/BUILDING.md

View File

@ -6,6 +6,28 @@ module.exports = function (grunt) {
pkg: grunt.file.readJSON('package.json'),
copy: {
main: {
expand: true,
cwd: 'src',
src: ['**/*','!**/*.coffee'],
dest: 'dist/',
}
},
coffee: {
main: {
options: {
sourceMap: true
},
expand: true,
cwd: 'src',
src: ['**/*.coffee'],
dest: 'dist/',
ext: '.js'
}
},
simplemocha: {
options: {
globals: ['expect', 'should'],
@ -27,7 +49,10 @@ module.exports = function (grunt) {
}
},
clean: ['test/sandbox'],
clean: {
test: ['test/sandbox'],
dist: ['dist']
},
yuidoc: {
compile: {
@ -37,7 +62,6 @@ module.exports = function (grunt) {
url: '<%= pkg.homepage %>',
options: {
paths: 'src/',
//themedir: 'path/to/custom/theme/',
outdir: 'docs/'
}
}
@ -46,25 +70,42 @@ module.exports = function (grunt) {
jshint: {
options: {
laxcomma: true,
expr: true
expr: true,
eqnull: true
},
all: ['Gruntfile.js', 'src/**/*.js', 'test/*.js']
all: ['Gruntfile.js', 'dist/cli/**/*.js', 'test/*.js']
}
};
grunt.initConfig( opts );
grunt.loadNpmTasks('grunt-contrib-coffee');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-simple-mocha');
grunt.loadNpmTasks('grunt-contrib-yuidoc');
grunt.loadNpmTasks('grunt-jsdoc');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.registerTask('test', 'Test the HackMyResume library.',
function( config ) { grunt.task.run(['clean','jshint','simplemocha:all']); });
grunt.registerTask('document', 'Generate HackMyResume library documentation.',
function( config ) { grunt.task.run( ['jsdoc'] ); });
grunt.registerTask('default', [ 'test', 'jsdoc' ]);
// Use 'grunt test' for local testing
grunt.registerTask('test', 'Test the HackMyResume application.',
function( config ) {
grunt.task.run(['clean:test','build','jshint','simplemocha:all']);
});
// Use 'grunt document' to build docs
grunt.registerTask('document', 'Generate HackMyResume documentation.',
function( config ) {
grunt.task.run( ['jsdoc'] );
});
// Use 'grunt build' to build HMR
grunt.registerTask('build', 'Build the HackMyResume application.',
function( config ) {
grunt.task.run( ['clean:dist','copy','coffee'] );
});
// Default task does everything
grunt.registerTask('default', [ 'test', 'document' ]);
};

View File

@ -1,7 +1,7 @@
The MIT License
===============
Copyright (c) 2016 hacksalot (https://github.com/hacksalot)
Copyright (c) 2015-2016 hacksalot (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

View File

@ -4,6 +4,7 @@ HackMyResume
[![Latest release][img-release]][latest-release]
[![Build status (MASTER)][img-master]][travis-url-master]
[![Build status (DEV)][img-dev]][travis-url-dev]
[![Join the chat at https://gitter.im/hacksalot/HackMyResume](https://badges.gitter.im/hacksalot/HackMyResume.svg)](https://gitter.im/hacksalot/HackMyResume?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
*Create polished résumés and CVs in multiple formats from your command line or
shell. Author in clean Markdown and JSON, export to Word, HTML, PDF, LaTeX,

30
dist/cli/analyze.hbs vendored Normal file
View File

@ -0,0 +1,30 @@
{{style "SECTIONS (" "bold"}}{{style totals.numSections "white" }}{{style ")" "bold"}}
employment: {{v totals.totals.employment "-" 2 "bold" }}
projects: {{v totals.totals.projects "-" 2 "bold" }}
education: {{v totals.totals.education "-" 2 "bold" }}
service: {{v totals.totals.service "-" 2 "bold" }}
skills: {{v totals.totals.skills "-" 2 "bold" }}
writing: {{v totals.totals.writing "-" 2 "bold" }}
speaking: {{v totals.totals.speaking "-" 2 "bold" }}
reading: {{v totals.totals.reading "-" 2 "bold" }}
social: {{v totals.totals.social "-" 2 "bold" }}
references: {{v totals.totals.references "-" 2 "bold" }}
testimonials: {{v totals.totals.testimonials "-" 2 "bold" }}
languages: {{v totals.totals.languages "-" 2 "bold" }}
interests: {{v totals.totals.interests "-" 2 "bold" }}
{{style "COVERAGE (" "bold"}}{{style coverage.pct "white"}}{{style ")" "bold"}}
Total Days: {{v coverage.duration.total "-" 5 "bold" }}
Employed: {{v coverage.duration.work "-" 5 "bold" }}
Gaps: {{v coverage.gaps.length "-" 5 "bold" }} [{{#if coverage.gaps.length }}{{#each coverage.gaps }}{{#unless @first}} {{/unless}}{{gapLength duration }}{{/each}}{{/if}}]
Overlaps: {{v coverage.overlaps.length "-" 5 "bold" }} [{{#if coverage.overlaps.length }}{{#each coverage.overlaps }}{{#unless @first}} {{/unless}}{{gapLength duration }}{{/each}}{{/if}}]
{{style "KEYWORDS (" "bold"}}{{style keywords.length "white" }}{{style ")" "bold"}}
{{#each keywords }}{{{pad name 18}}}: {{v count "-" 5 "bold"}} mention{{#isPlural count}}s{{/isPlural}}
{{/each}}
-------------------------------
{{v keywords.length "0" 9 "bold"}} {{style "KEYWORDS" "bold"}} {{v keywords.totalKeywords "0" 5 "bold"}} {{style "mentions" "bold"}}

248
dist/cli/error.js vendored Normal file
View File

@ -0,0 +1,248 @@
/**
Error-handling routines for HackMyResume.
@module cli/error
@license MIT. See LICENSE.md for details.
*/
(function() {
var ErrorHandler, FCMD, FS, HMSTATUS, M2C, PATH, PKG, SyntaxErrorEx, WRAP, YAML, _defaultLog, assembleError, chalk, extend, printf;
HMSTATUS = require('../core/status-codes');
PKG = require('../../package.json');
FS = require('fs');
FCMD = require('../index');
PATH = require('path');
WRAP = require('word-wrap');
M2C = require('../utils/md2chalk');
chalk = require('chalk');
extend = require('extend');
YAML = require('yamljs');
printf = require('printf');
SyntaxErrorEx = require('../utils/syntax-error-ex');
require('string.prototype.startswith');
/** Error handler for HackMyResume. All errors are handled here.
@class ErrorHandler
*/
ErrorHandler = module.exports = {
init: function(debug, assert, silent) {
this.debug = debug;
this.assert = assert;
this.silent = silent;
this.msgs = require('./msg').errors;
return this;
},
err: function(ex, shouldExit) {
var o, objError, stack, stackTrace;
o = this.silent ? function() {} : _defaultLog;
if (ex.pass) {
throw ex;
}
this.msgs = this.msgs || require('./msg').errors;
if (ex.fluenterror) {
objError = assembleError.call(this, ex);
o(this['format_' + objError.etype](objError.msg));
if (objError.withStack) {
stack = ex.stack || (ex.inner && ex.inner.stack);
stack && o(chalk.gray(stack));
}
if (shouldExit) {
if (this.debug) {
o(chalk.cyan('Exiting with error code ' + ex.fluenterror.toString()));
}
if (this.assert) {
ex.pass = true;
throw ex;
}
return process.exit(ex.fluenterror);
}
} else {
o(ex);
stackTrace = ex.stack || (ex.inner && ex.inner.stack);
if (stackTrace && this.debug) {
return o(M2C(ex.stack || ex.inner.stack, 'gray'));
}
}
},
format_error: function(msg) {
msg = msg || '';
return chalk.red.bold(msg.toUpperCase().startsWith('ERROR:') ? msg : 'Error: ' + msg);
},
format_warning: function(brief, msg) {
return chalk.yellow(brief) + chalk.yellow(msg || '');
},
format_custom: function(msg) {
return msg;
}
};
_defaultLog = function() {
return console.log.apply(console.log, arguments);
};
assembleError = function(ex) {
var etype, msg, quit, se, withStack;
msg = '';
withStack = false;
quit = false;
etype = 'warning';
if (this.debug) {
withStack = true;
}
switch (ex.fluenterror) {
case HMSTATUS.themeNotFound:
msg = printf(M2C(this.msgs.themeNotFound.msg, 'yellow'), ex.data);
break;
case HMSTATUS.copyCSS:
msg = M2C(this.msgs.copyCSS.msg, 'red');
quit = false;
break;
case HMSTATUS.resumeNotFound:
msg = M2C(this.msgs.resumeNotFound.msg, 'yellow');
break;
case HMSTATUS.missingCommand:
msg = M2C(this.msgs.missingCommand.msg + " (", 'yellow');
msg += Object.keys(FCMD.verbs).map(function(v, idx, ar) {
return (idx === ar.length - 1 ? chalk.yellow('or ') : '') + chalk.yellow.bold(v.toUpperCase());
}).join(chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += chalk.gray(FS.readFileSync(PATH.resolve(__dirname, '../cli/use.txt'), 'utf8'));
break;
case HMSTATUS.invalidCommand:
msg = printf(M2C(this.msgs.invalidCommand.msg, 'yellow'), ex.attempted);
break;
case HMSTATUS.resumeNotFoundAlt:
msg = M2C(this.msgs.resumeNotFoundAlt.msg, 'yellow');
break;
case HMSTATUS.inputOutputParity:
msg = M2C(this.msgs.inputOutputParity.msg);
break;
case HMSTATUS.createNameMissing:
msg = M2C(this.msgs.createNameMissing.msg);
break;
case HMSTATUS.pdfGeneration:
msg = M2C(this.msgs.pdfGeneration.msg, 'bold');
if (ex.inner) {
msg += chalk.red('\n' + ex.inner);
}
quit = false;
etype = 'error';
break;
case HMSTATUS.invalid:
msg = M2C(this.msgs.invalid.msg, 'red');
etype = 'error';
break;
case HMSTATUS.generateError:
msg = (ex.inner && ex.inner.toString()) || ex;
quit = false;
etype = 'error';
break;
case HMSTATUS.fileSaveError:
msg = printf(M2C(this.msgs.fileSaveError.msg), (ex.inner || ex).toString());
etype = 'error';
quit = false;
break;
case HMSTATUS.invalidFormat:
ex.data.forEach(function(d) {
return msg += printf(M2C(this.msgs.invalidFormat.msg, 'bold'), ex.theme.name.toUpperCase(), d.format.toUpperCase());
}, this);
break;
case HMSTATUS.missingParam:
msg = printf(M2C(this.msgs.missingParam.msg), ex.expected, ex.helper);
break;
case HMSTATUS.invalidHelperUse:
msg = printf(M2C(this.msgs.invalidHelperUse.msg), ex.helper);
if (ex.error) {
msg += '\n--> ' + assembleError.call(this, extend(true, {}, ex, {
fluenterror: ex.error
})).msg;
}
quit = false;
etype = 'warning';
break;
case HMSTATUS.notOnPath:
msg = printf(M2C(this.msgs.notOnPath.msg, 'bold'), ex.engine);
quit = false;
etype = 'error';
break;
case HMSTATUS.readError:
if (!ex.quiet) {
console.error(printf(M2C(this.msgs.readError.msg, 'red'), ex.file));
}
msg = ex.inner.toString();
etype = 'error';
break;
case HMSTATUS.mixedMerge:
msg = M2C(this.msgs.mixedMerge.msg);
quit = false;
break;
case HMSTATUS.invokeTemplate:
msg = M2C(this.msgs.invokeTemplate.msg, 'red');
msg += M2C('\n' + WRAP(ex.inner.toString(), {
width: 60,
indent: ' '
}), 'gray');
etype = 'custom';
break;
case HMSTATUS.compileTemplate:
etype = 'error';
break;
case HMSTATUS.themeLoad:
msg = M2C(printf(this.msgs.themeLoad.msg, ex.attempted.toUpperCase()), 'red');
if (ex.inner && ex.inner.fluenterror) {
msg += M2C('\nError: ', 'red') + assembleError.call(this, ex.inner).msg;
}
quit = true;
etype = 'custom';
break;
case HMSTATUS.parseError:
if (SyntaxErrorEx.is(ex.inner)) {
console.error(printf(M2C(this.msgs.readError.msg, 'red'), ex.file));
se = new SyntaxErrorEx(ex, ex.raw);
if ((se.line != null) && (se.col != null)) {
msg = printf(M2C(this.msgs.parseError.msg[0], 'red'), se.line, se.col);
} else if (se.line != null) {
msg = printf(M2C(this.msgs.parseError.msg[1], 'red'), se.line);
} else {
msg = M2C(this.msgs.parseError.msg[2], 'red');
}
} else if (ex.inner && (ex.inner.line != null) && (ex.inner.col != null)) {
msg = printf(M2C(this.msgs.parseError.msg[0], 'red'), ex.inner.line, ex.inner.col);
} else {
msg = ex;
}
etype = 'error';
break;
case HMSTATUS.createError:
msg = printf(M2C(this.msgs.createError.msg), ex.inner.path);
etype = 'error';
break;
case HMSTATUS.validateError:
msg = printf(M2C(this.msgs.validateError.msg), ex.inner.toString());
etype = 'error';
}
return {
msg: msg,
withStack: withStack,
quit: quit,
etype: etype
};
};
}).call(this);
//# sourceMappingURL=error.js.map

View File

@ -12,11 +12,11 @@ Command-line interface (CLI) for HackMyResume.
try {
require('./cli/main')( process.argv );
require('./main')( process.argv );
}
catch( ex ) {
require('./cli/error').err( ex, true );
require('./error').err( ex, true );
}

316
dist/cli/main.js vendored Normal file
View File

@ -0,0 +1,316 @@
/**
Definition of the `main` function.
@module cli/main
@license MIT. See LICENSE.md for details.
*/
(function() {
var Command, EXTEND, FS, HME, HMR, HMSTATUS, M2C, OUTPUT, PAD, PATH, PKG, StringUtils, _, _err, _exitCallback, _opts, _out, _title, chalk, execute, executeFail, executeSuccess, initOptions, initialize, loadOptions, logMsg, main, printf, safeLoadJSON, splitSrcDest;
HMR = require('../index');
PKG = require('../../package.json');
FS = require('fs');
EXTEND = require('extend');
chalk = require('chalk');
PATH = require('path');
HMSTATUS = require('../core/status-codes');
HME = require('../core/event-codes');
safeLoadJSON = require('../utils/safe-json-loader');
StringUtils = require('../utils/string.js');
_ = require('underscore');
OUTPUT = require('./out');
PAD = require('string-padding');
Command = require('commander').Command;
M2C = require('../utils/md2chalk');
printf = require('printf');
_opts = {};
_title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***');
_out = new OUTPUT(_opts);
_err = require('./error');
_exitCallback = null;
/*
A callable implementation of the HackMyResume CLI. Encapsulates the command
line interface as a single method accepting a parameter array.
@alias module:cli/main.main
@param rawArgs {Array} An array of command-line parameters. Will either be
process.argv (in production) or custom parameters (in test).
*/
main = module.exports = function(rawArgs, exitCallback) {
var args, initInfo, program;
initInfo = initialize(rawArgs, exitCallback);
args = initInfo.args;
program = new Command('hackmyresume').version(PKG.version).description(chalk.yellow.bold('*** HackMyResume ***')).option('-s --silent', 'Run in silent mode').option('--no-color', 'Disable colors').option('--color', 'Enable colors').option('-d --debug', 'Enable diagnostics', false).option('-a --assert', 'Treat warnings as errors', false).option('-v --version', 'Show the version').allowUnknownOption();
program.jsonArgs = initInfo.options;
program.command('new')["arguments"]('<sources...>').option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH').alias('create').description('Create resume(s) in FRESH or JSON RESUME format.').action((function(sources) {
execute.call(this, sources, [], this.opts(), logMsg);
}));
program.command('validate')["arguments"]('<sources...>').description('Validate a resume in FRESH or JSON RESUME format.').action(function(sources) {
execute.call(this, sources, [], this.opts(), logMsg);
});
program.command('convert').description('Convert a resume to/from FRESH or JSON RESUME format.').action(function() {
var x;
x = splitSrcDest.call(this);
execute.call(this, x.src, x.dst, this.opts(), logMsg);
});
program.command('analyze')["arguments"]('<sources...>').description('Analyze one or more resumes.').action(function(sources) {
execute.call(this, sources, [], this.opts(), logMsg);
});
program.command('peek')["arguments"]('<sources...>').description('Peek at a resume field or section').action(function(sources, sectionOrField) {
var dst;
dst = sources && sources.length > 1 ? [sources.pop()] : [];
execute.call(this, sources, dst, this.opts(), logMsg);
});
program.command('build').alias('generate').option('-t --theme <theme>', 'Theme name or path').option('-n --no-prettify', 'Disable HTML prettification', true).option('-c --css <option>', 'CSS linking / embedding').option('-p --pdf <engine>', 'PDF generation engine').option('--no-sort', 'Sort resume sections by date', false).option('--tips', 'Display theme tips and warnings.', false).description('Generate resume to multiple formats').action(function(sources, targets, options) {
var x;
x = splitSrcDest.call(this);
execute.call(this, x.src, x.dst, this.opts(), logMsg);
});
program.parse(args);
if (!program.args.length) {
throw {
fluenterror: 4
};
}
};
/* Massage command-line args and setup Commander.js. */
initialize = function(ar, exitCallback) {
var o;
_exitCallback = exitCallback || process.exit;
o = initOptions(ar);
o.silent || logMsg(_title);
if (o.debug) {
_out.log(chalk.cyan('The -d or --debug switch was specified. DEBUG mode engaged.'));
_out.log('');
_out.log(chalk.cyan(PAD(' Platform:', 25, null, PAD.RIGHT)) + chalk.cyan.bold(process.platform === 'win32' ? 'windows' : process.platform));
_out.log(chalk.cyan(PAD(' Node.js:', 25, null, PAD.RIGHT)) + chalk.cyan.bold(process.version));
_out.log(chalk.cyan(PAD(' HackMyResume:', 25, null, PAD.RIGHT)) + chalk.cyan.bold('v' + PKG.version));
_out.log(chalk.cyan(PAD(' FRESCA:', 25, null, PAD.RIGHT)) + chalk.cyan.bold(PKG.dependencies.fresca));
_out.log('');
}
_err.init(o.debug, o.assert, o.silent);
if (o.verb && !HMR.verbs[o.verb] && !HMR.alias[o.verb]) {
_err.err({
fluenterror: HMSTATUS.invalidCommand,
quit: true,
attempted: o.orgVerb
}, true);
}
Command.prototype.missingArgument = function(name) {
_err.err({
fluenterror: this.name() !== 'new' ? HMSTATUS.resumeNotFound : HMSTATUS.createNameMissing
}, true);
};
Command.prototype.helpInformation = function() {
var manPage;
manPage = FS.readFileSync(PATH.join(__dirname, 'use.txt'), 'utf8');
return chalk.green.bold(manPage);
};
return {
args: o.args,
options: o.json
};
};
/* Init options prior to setting up command infrastructure. */
initOptions = function(ar) {
oVerb;
var args, cleanArgs, inf, isAssert, isDebug, isMono, isSilent, oJSON, oVerb, optStr, optsIdx, verb, vidx;
verb = '';
args = ar.slice();
cleanArgs = args.slice(2);
oJSON;
if (cleanArgs.length) {
vidx = _.findIndex(cleanArgs, function(v) {
return v[0] !== '-';
});
if (vidx !== -1) {
oVerb = cleanArgs[vidx];
verb = args[vidx + 2] = oVerb.trim().toLowerCase();
}
optsIdx = _.findIndex(cleanArgs, function(v) {
return v === '-o' || v === '--options' || v === '--opts';
});
if (optsIdx !== -1) {
optStr = cleanArgs[optsIdx + 1];
args.splice(optsIdx + 2, 2);
if (optStr && (optStr = optStr.trim())) {
if (optStr[0] === '{') {
/* jshint ignore:start */
oJSON = eval('(' + optStr + ')');
/* jshint ignore:end */
} else {
inf = safeLoadJSON(optStr);
if (!inf.ex) {
oJSON = inf.json;
}
}
}
}
}
isDebug = _.some(args, function(v) {
return v === '-d' || v === '--debug';
});
isSilent = _.some(args, function(v) {
return v === '-s' || v === '--silent';
});
isAssert = _.some(args, function(v) {
return v === '-a' || v === '--assert';
});
isMono = _.some(args, function(v) {
return v === '--no-color';
});
return {
color: !isMono,
debug: isDebug,
silent: isSilent,
assert: isAssert,
orgVerb: oVerb,
verb: verb,
json: oJSON,
args: args
};
};
/* Invoke a HackMyResume verb. */
execute = function(src, dst, opts, log) {
var prom, v;
v = new HMR.verbs[this.name()]();
loadOptions.call(this, opts, this.parent.jsonArgs);
_opts.errHandler = v;
_out.init(_opts);
v.on('hmr:status', function() {
return _out["do"].apply(_out, arguments);
});
v.on('hmr:error', function() {
return _err.err.apply(_err, arguments);
});
prom = v.invoke.call(v, src, dst, _opts, log);
prom.then(executeSuccess, executeFail);
};
/* Success handler for verb invocations. Calls process.exit by default */
executeSuccess = function(obj) {};
/* Failure handler for verb invocations. Calls process.exit by default */
executeFail = function(err) {
var finalErrorCode, msgs;
finalErrorCode = -1;
if (err) {
finalErrorCode = err.fluenterror ? err.fluenterror : err;
}
if (_opts.debug) {
msgs = require('./msg').errors;
logMsg(printf(M2C(msgs.exiting.msg, 'cyan'), finalErrorCode));
if (err.stack) {
logMsg(err.stack);
}
}
_exitCallback(finalErrorCode);
};
/*
Initialize HackMyResume options.
TODO: Options loading is a little hacky, for two reasons:
- Commander.js idiosyncracies
- Need to accept JSON inputs from the command line.
*/
loadOptions = function(o, cmdO) {
if (cmdO) {
o = EXTEND(true, o, cmdO);
}
o = EXTEND(true, o, this.opts());
if (this.parent.silent !== void 0 && this.parent.silent !== null) {
o.silent = this.parent.silent;
}
if (this.parent.debug !== void 0 && this.parent.debug !== null) {
o.debug = this.parent.debug;
}
if (this.parent.assert !== void 0 && this.parent.assert !== null) {
o.assert = this.parent.assert;
}
if (o.debug) {
logMsg(chalk.cyan('OPTIONS:') + '\n');
_.each(o, function(val, key) {
return logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'), PAD(key, 22, null, PAD.RIGHT), val);
});
logMsg('');
}
EXTEND(true, _opts, o);
};
/* Split multiple command-line filenames by the 'TO' keyword */
splitSrcDest = function() {
var params, splitAt;
params = this.parent.args.filter(function(j) {
return String.is(j);
});
if (params.length === 0) {
throw {
fluenterror: HMSTATUS.resumeNotFound,
quit: true
};
}
splitAt = _.findIndex(params, function(p) {
return p.toLowerCase() === 'to';
});
if (splitAt === params.length - 1 && splitAt !== -1) {
logMsg(chalk.yellow('Please ') + chalk.yellow.bold('specify an output file') + chalk.yellow(' for this operation or ') + chalk.yellow.bold('omit the TO keyword') + chalk.yellow('.'));
return;
}
return {
src: params.slice(0, splitAt === -1 ? void 0 : splitAt),
dst: splitAt === -1 ? [] : params.slice(splitAt + 1)
};
};
/* Simple logging placeholder. */
logMsg = function() {
return _opts.silent || console.log.apply(console.log, arguments);
};
}).call(this);
//# sourceMappingURL=main.js.map

19
dist/cli/msg.js vendored Normal file
View File

@ -0,0 +1,19 @@
/**
Message-handling routines for HackMyResume.
@module cli/msg
@license MIT. See LICENSE.md for details.
*/
(function() {
var PATH, YAML;
PATH = require('path');
YAML = require('yamljs');
module.exports = YAML.load(PATH.join(__dirname, 'msg.yml'));
}).call(this);
//# sourceMappingURL=msg.js.map

111
dist/cli/msg.yml vendored Normal file
View File

@ -0,0 +1,111 @@
events:
begin:
msg: Invoking **%s** command.
beforeCreate:
msg: Creating new **%s** resume: **%s**
afterCreate:
msg: Creating new **%s** resume: **%s**
afterRead:
msg: Reading **%s** resume: **%s**
beforeTheme:
msg: Verifying **%s** theme.
afterTheme:
msg: Verifying outputs: ???
beforeMerge:
msg:
- "Merging **%s**"
- " onto **%s**"
applyTheme:
msg: Applying **%s** theme (**%s** format%s)
afterBuild:
msg:
- "The **%s** theme says:"
- |
"For best results view JSON Resume themes over a
local or remote HTTP connection. For example:
npm install http-server -g
http-server <resume-folder>
For more information, see the README."
afterGenerate:
msg:
- " (with %s)"
- "Skipping %s resume: %s"
- "Generating **%s** resume: **%s**"
beforeAnalyze:
msg: "Analyzing **%s** resume: **%s**"
beforeConvert:
msg: "Converting **%s** (**%s**) to **%s** (**%s**)"
afterValidate:
msg:
- "Validating **%s** against the **%s** schema: "
- "VALID!"
- "INVALID"
- "BROKEN"
- "MISSING"
- "ERROR"
beforePeek:
msg:
- Peeking at **%s** in **%s**
- Peeking at **%s**
afterPeek:
msg: "The specified key **%s** was not found in **%s**."
afterInlineConvert:
msg: Converting **%s** to **%s** format.
errors:
themeNotFound:
msg: >
**Couldn't find the '%s' theme.** Please specify the name of a preinstalled
FRESH theme or the path to a locally installed FRESH or JSON Resume theme.
copyCSS:
msg: Couldn't copy CSS file to destination folder.
resumeNotFound:
msg: Please **feed me a resume** in FRESH or JSON Resume format.
missingCommand:
msg: Please **give me a command**
invalidCommand:
msg: Invalid command: '%s'
resumeNotFoundAlt:
msg: Please **feed me a resume** in either FRESH or JSON Resume format.
inputOutputParity:
msg: Please **specify an output file name** for every input file you wish to convert.
createNameMissing:
msg: Please **specify the filename** of the resume to create.
pdfGeneration:
msg: PDF generation failed. Make sure wkhtmltopdf is installed and accessible from your path.
invalid:
msg: Validation failed and the --assert option was specified.
invalidFormat:
msg: The **%s** theme doesn't support the **%s** format.
notOnPath:
msg: %s wasn't found on your system path or is inaccessible. PDF not generated.
readError:
msg: Reading **???** resume: **%s**
parseError:
msg:
- Invalid or corrupt JSON on line %s column %s.
- Invalid or corrupt JSON on line %s.
- Invalid or corrupt JSON.
invalidHelperUse:
msg: "**Warning**: Incorrect use of the **%s** theme helper."
fileSaveError:
msg: An error occurred while writing %s to disk: %s.
mixedMerge:
msg: "**Warning:** merging mixed resume types. Errors may occur."
invokeTemplate:
msg: "An error occurred during template invocation."
compileTemplate:
msg: "An error occurred during template compilation."
themeLoad:
msg: "Applying **%s** theme (? formats)"
invalidParamCount:
msg: "Invalid number of parameters. Expected: **%s**."
missingParam:
msg: The '**%s**' parameter was needed but not supplied.
createError:
msg: Failed to create **'%s'**.
exiting:
msg: Exiting with status code **%s**.
validateError:
msg: "An error occurred during validation:\n%s"

193
dist/cli/out.js vendored Normal file
View File

@ -0,0 +1,193 @@
/**
Output routines for HackMyResume.
@license MIT. See LICENSE.md for details.
@module cli/out
*/
(function() {
var EXTEND, FS, HANDLEBARS, HME, LO, M2C, OutputHandler, PATH, YAML, _, chalk, dbgStyle, pad, printf;
chalk = require('chalk');
HME = require('../core/event-codes');
_ = require('underscore');
M2C = require('../utils/md2chalk.js');
PATH = require('path');
LO = require('lodash');
FS = require('fs');
EXTEND = require('extend');
HANDLEBARS = require('handlebars');
YAML = require('yamljs');
printf = require('printf');
pad = require('string-padding');
dbgStyle = 'cyan';
/** A stateful output module. All HMR console output handled here. */
module.exports = OutputHandler = (function() {
function OutputHandler(opts) {
this.init(opts);
return;
}
OutputHandler.prototype.init = function(opts) {
this.opts = EXTEND(true, this.opts || {}, opts);
this.msgs = YAML.load(PATH.join(__dirname, 'msg.yml')).events;
};
OutputHandler.prototype.log = function(msg) {
var finished;
msg = msg || '';
printf = require('printf');
finished = printf.apply(printf, arguments);
return this.opts.silent || console.log(finished);
};
OutputHandler.prototype["do"] = function(evt) {
var L, WRAP, adj, info, msg, msgs, numFormats, output, rawTpl, sty, style, suffix, template, that, themeName, tot;
that = this;
L = function() {
return that.log.apply(that, arguments);
};
switch (evt.sub) {
case HME.begin:
return this.opts.debug && L(M2C(this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase());
case HME.afterCreate:
L(M2C(this.msgs.beforeCreate.msg, evt.isError ? 'red' : 'green'), evt.fmt, evt.file);
break;
case HME.beforeTheme:
return this.opts.debug && L(M2C(this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase());
case HME.afterParse:
return L(M2C(this.msgs.afterRead.msg, 'gray', 'white.dim'), evt.fmt.toUpperCase(), evt.file);
case HME.beforeMerge:
msg = '';
evt.f.reverse().forEach(function(a, idx) {
return msg += printf((idx === 0 ? this.msgs.beforeMerge.msg[0] : this.msgs.beforeMerge.msg[1]), a.file);
}, this);
return L(M2C(msg, (evt.mixed ? 'yellow' : 'gray'), 'white.dim'));
case HME.applyTheme:
this.theme = evt.theme;
numFormats = Object.keys(evt.theme.formats).length;
return L(M2C(this.msgs.applyTheme.msg, evt.status === 'error' ? 'red' : 'gray', evt.status === 'error' ? 'bold' : 'white.dim'), evt.theme.name.toUpperCase(), numFormats, numFormats === 1 ? '' : 's');
case HME.end:
if (evt.cmd === 'build') {
themeName = this.theme.name.toUpperCase();
if (this.opts.tips && (this.theme.message || this.theme.render)) {
WRAP = require('word-wrap');
if (this.theme.message) {
L(M2C(this.msgs.afterBuild.msg[0], 'cyan'), themeName);
return L(M2C(this.theme.message, 'white'));
} else if (this.theme.render) {
L(M2C(this.msgs.afterBuild.msg[0], 'cyan'), themeName);
return L(M2C(this.msgs.afterBuild.msg[1], 'white'));
}
}
}
break;
case HME.afterGenerate:
suffix = '';
if (evt.fmt === 'pdf') {
if (this.opts.pdf) {
if (this.opts.pdf !== 'none') {
suffix = printf(M2C(this.msgs.afterGenerate.msg[0], evt.error ? 'red' : 'green'), this.opts.pdf);
} else {
L(M2C(this.msgs.afterGenerate.msg[1], 'gray'), evt.fmt.toUpperCase(), evt.file);
return;
}
}
}
return L(M2C(this.msgs.afterGenerate.msg[2] + suffix, evt.error ? 'red' : 'green'), pad(evt.fmt.toUpperCase(), 4, null, pad.RIGHT), PATH.relative(process.cwd(), evt.file));
case HME.beforeAnalyze:
return L(M2C(this.msgs.beforeAnalyze.msg, 'green'), evt.fmt, evt.file);
case HME.afterAnalyze:
info = evt.info;
rawTpl = FS.readFileSync(PATH.join(__dirname, 'analyze.hbs'), 'utf8');
HANDLEBARS.registerHelper(require('../helpers/console-helpers'));
template = HANDLEBARS.compile(rawTpl, {
strict: false,
assumeObjects: false
});
tot = 0;
info.keywords.forEach(function(g) {
return tot += g.count;
});
info.keywords.totalKeywords = tot;
output = template(info);
return this.log(chalk.cyan(output));
case HME.beforeConvert:
return L(M2C(this.msgs.beforeConvert.msg, 'green'), evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt);
case HME.afterInlineConvert:
return L(M2C(this.msgs.afterInlineConvert.msg, 'gray', 'white.dim'), evt.file, evt.fmt);
case HME.afterValidate:
style = 'red';
adj = '';
msgs = this.msgs.afterValidate.msg;
switch (evt.status) {
case 'valid':
style = 'green';
adj = msgs[1];
break;
case 'invalid':
style = 'yellow';
adj = msgs[2];
break;
case 'broken':
style = 'red';
adj = msgs[3];
break;
case 'missing':
style = 'red';
adj = msgs[4];
break;
case 'unknown':
style = 'red';
adj = msgs[5];
}
evt.schema = evt.schema.replace('jars', 'JSON Resume').toUpperCase();
L(M2C(msgs[0], 'white') + chalk[style].bold(adj), evt.file, evt.schema);
if (evt.violations) {
_.each(evt.violations, function(err, idx) {
L(chalk.yellow.bold('--> ') + chalk.yellow(err.field.replace('data.', 'resume.').toUpperCase() + ' ' + err.message));
}, this);
}
break;
case HME.afterPeek:
sty = evt.error ? 'red' : (evt.target !== void 0 ? 'green' : 'yellow');
if (evt.requested) {
L(M2C(this.msgs.beforePeek.msg[0], sty), evt.requested, evt.file);
} else {
L(M2C(this.msgs.beforePeek.msg[1], sty), evt.file);
}
if (evt.target !== void 0 && !evt.error) {
return console.dir(evt.target, {
depth: null,
colors: true
});
} else if (!evt.error) {
return L(M2C(this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file);
} else if (evt.error) {
return L(chalk.red(evt.error.inner.inner));
}
}
};
return OutputHandler;
})();
}).call(this);
//# sourceMappingURL=out.js.map

51
dist/cli/use.txt vendored Normal file
View File

@ -0,0 +1,51 @@
Usage:
hackmyresume <command> <sources> [TO <targets>] [<options>]
Available commands:
BUILD Build your resume to the destination format(s).
ANALYZE Analyze your resume for keywords, gaps, and metrics.
VALIDATE Validate your resume for errors and typos.
CONVERT Convert your resume between FRESH and JSON Resume.
NEW Create a new resume in FRESH or JSON Resume format.
PEEK View a specific field or element on your resume.
Available options:
--theme -t Path to a FRESH or JSON Resume theme.
--pdf -p Specify the PDF engine to use (wkhtmltopdf or phantom).
--options -o Load options from an external JSON file.
--format -f The format (FRESH or JSON Resume) to use.
--debug -d Emit extended debugging info.
--assert -a Treat resume validation warnings as errors.
--no-colors Disable terminal colors.
--tips Display theme messages and tips.
--help -h Display help documentation.
--version -v Display the current version.
Not all options are supported for all commands. For example, the
--theme option is only supported for the BUILD command.
Examples:
hackmyresume BUILD resume.json TO out/resume.all --theme modern
hackmyresume ANALYZE resume.json
hackmyresume NEW my-new-resume.json --format JRS
hackmyresume CONVERT resume-fresh.json TO resume-jrs.json
hackmyresume VALIDATE resume.json
hackmyresume PEEK resume.json employment[2].summary
Tips:
- You can specify multiple sources and/or targets for all commands.
- You can use any FRESH or JSON Resume theme with HackMyResume.
- Specify a file extension of .all to generate your resume to all
available formats supported by the theme. (BUILD command.)
- The --theme parameter can specify either the name of a preinstalled
theme, or the path to a local FRESH or JSON Resume theme.
- Visit https://www.npmjs.com/search?q=jsonresume-theme for a full
listing of all available JSON Resume themes.
- Visit https://github.com/fluentdesk/fresh-themes for a complete
listing of all available FRESH themes.
- Report bugs to https://githut.com/hacksalot/HackMyResume/issues.

73
dist/core/abstract-resume.js vendored Normal file
View File

@ -0,0 +1,73 @@
/**
Definition of the AbstractResume class.
@license MIT. See LICENSE.md for details.
@module core/abstract-resume
*/
(function() {
var AbstractResume, FluentDate, _, __;
_ = require('underscore');
__ = require('lodash');
FluentDate = require('./fluent-date');
AbstractResume = (function() {
function AbstractResume() {}
/**
Compute the total duration of the work history.
@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.
*/
AbstractResume.prototype.duration = function(collKey, startKey, endKey, unit) {
var firstDate, hist, lastDate, new_e;
unit = unit || 'years';
hist = __.get(this, collKey);
if (!hist || !hist.length) {
return 0;
}
new_e = hist.map(function(job) {
var obj;
obj = _.pick(job, [startKey, endKey]);
if (!_.has(obj, endKey)) {
obj[endKey] = 'current';
}
if (obj && (obj[startKey] || obj[endKey])) {
obj = _.pairs(obj);
obj[0][1] = FluentDate.fmt(obj[0][1]);
if (obj.length > 1) {
obj[1][1] = FluentDate.fmt(obj[1][1]);
}
}
return obj;
});
new_e = _.filter(_.flatten(new_e, true), function(v) {
return v && v.length && v[0] && v[0].length;
});
if (!new_e || !new_e.length) {
return 0;
}
new_e = _.sortBy(new_e, function(elem) {
return elem[1].unix();
});
firstDate = _.first(new_e)[1];
lastDate = _.last(new_e)[1];
return lastDate.diff(firstDate, unit);
};
return AbstractResume;
})();
module.exports = AbstractResume;
}).call(this);
//# sourceMappingURL=abstract-resume.js.map

62
dist/core/default-formats.js vendored Normal file
View File

@ -0,0 +1,62 @@
/*
Event code definitions.
@module core/default-formats
@license MIT. See LICENSE.md for details.
*/
/** Supported resume formats. */
(function() {
module.exports = [
{
name: 'html',
ext: 'html',
gen: new (require('../generators/html-generator'))()
}, {
name: 'txt',
ext: 'txt',
gen: new (require('../generators/text-generator'))()
}, {
name: 'doc',
ext: 'doc',
fmt: 'xml',
gen: new (require('../generators/word-generator'))()
}, {
name: 'pdf',
ext: 'pdf',
fmt: 'html',
is: false,
gen: new (require('../generators/html-pdf-cli-generator'))()
}, {
name: 'png',
ext: 'png',
fmt: 'html',
is: false,
gen: new (require('../generators/html-png-generator'))()
}, {
name: 'md',
ext: 'md',
fmt: 'txt',
gen: new (require('../generators/markdown-generator'))()
}, {
name: 'json',
ext: 'json',
gen: new (require('../generators/json-generator'))()
}, {
name: 'yml',
ext: 'yml',
fmt: 'yml',
gen: new (require('../generators/json-yaml-generator'))()
}, {
name: 'latex',
ext: 'tex',
fmt: 'latex',
gen: new (require('../generators/latex-generator'))()
}
];
}).call(this);
//# sourceMappingURL=default-formats.js.map

20
dist/core/default-options.js vendored Normal file
View File

@ -0,0 +1,20 @@
/*
Event code definitions.
@module core/default-options
@license MIT. See LICENSE.md for details.
*/
(function() {
module.exports = {
theme: 'modern',
prettify: {
indent_size: 2,
unformatted: ['em', 'strong'],
max_char: 80
}
};
}).call(this);
//# sourceMappingURL=default-options.js.map

77
dist/core/empty-jrs.json vendored Normal file
View File

@ -0,0 +1,77 @@
{
"basics": {
"name": "",
"label": "",
"picture": "",
"email": "",
"phone": "",
"degree": "",
"website": "",
"summary": "",
"location": {
"address": "",
"postalCode": "",
"city": "",
"countryCode": "",
"region": ""
},
"profiles": [{
"network": "",
"username": "",
"url": ""
}]
},
"work": [{
"company": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [
""
]
}],
"awards": [{
"title": "",
"date": "",
"awarder": "",
"summary": ""
}],
"education": [{
"institution": "",
"area": "",
"studyType": "",
"startDate": "",
"endDate": "",
"gpa": "",
"courses": [ "" ]
}],
"publications": [{
"name": "",
"publisher": "",
"releaseDate": "",
"website": "",
"summary": ""
}],
"volunteer": [{
"organization": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [ "" ]
}],
"skills": [{
"name": "",
"level": "",
"keywords": [""]
}]
}

44
dist/core/event-codes.js vendored Normal file
View File

@ -0,0 +1,44 @@
/*
Event code definitions.
@module core/event-codes
@license MIT. See LICENSE.md for details.
*/
(function() {
module.exports = {
error: -1,
success: 0,
begin: 1,
end: 2,
beforeRead: 3,
afterRead: 4,
beforeCreate: 5,
afterCreate: 6,
beforeTheme: 7,
afterTheme: 8,
beforeMerge: 9,
afterMerge: 10,
beforeGenerate: 11,
afterGenerate: 12,
beforeAnalyze: 13,
afterAnalyze: 14,
beforeConvert: 15,
afterConvert: 16,
verifyOutputs: 17,
beforeParse: 18,
afterParse: 19,
beforePeek: 20,
afterPeek: 21,
beforeInlineConvert: 22,
afterInlineConvert: 23,
beforeValidate: 24,
afterValidate: 25,
beforeWrite: 26,
afterWrite: 27,
applyTheme: 28
};
}).call(this);
//# sourceMappingURL=event-codes.js.map

107
dist/core/fluent-date.js vendored Normal file
View File

@ -0,0 +1,107 @@
/**
The HackMyResume date representation.
@license MIT. See LICENSE.md for details.
@module core/fluent-date
*/
(function() {
var FluentDate, abbr, moment, months;
moment = require('moment');
require('../utils/string');
/**
Create a FluentDate from a string or Moment date object. There are a few date
formats to be aware of here.
1. The words "Present" and "Now", referring to the current date
2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10")
3. Year-and-month only ("2015-04")
4. Year-only "YYYY" ("2015")
5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008")
6. Empty dates ("", " ")
7. Any other date format that Moment.js can parse from
Note: Moment can transparently parse all or most of these, without requiring us
to specify a date format...but for maximum parsing safety and to avoid Moment
deprecation warnings, it's recommended to either a) explicitly specify the date
format or b) use an ISO format. For clarity, we handle these cases explicitly.
@class FluentDate
*/
FluentDate = (function() {
function FluentDate(dt) {
this.rep = this.fmt(dt);
}
FluentDate.isCurrent = function(dt) {
return !dt || (String.is(dt) && /^(present|now|current)$/.test(dt));
};
return FluentDate;
})();
months = {};
abbr = {};
moment.months().forEach(function(m, idx) {
return months[m.toLowerCase()] = idx + 1;
});
moment.monthsShort().forEach(function(m, idx) {
return abbr[m.toLowerCase()] = idx + 1;
});
abbr.sept = 9;
module.exports = FluentDate;
FluentDate.fmt = function(dt, throws) {
var month, mt, parts, ref, temp;
throws = (throws === void 0 || throws === null) || throws;
if (typeof dt === 'string' || dt instanceof String) {
dt = dt.toLowerCase().trim();
if (/^(present|now|current)$/.test(dt)) {
return moment();
} else if (/^\D+\s+\d{4}$/.test(dt)) {
parts = dt.split(' ');
month = months[parts[0]] || abbr[parts[0]];
temp = parts[1] + '-' + ((ref = month < 10) != null ? ref : '0' + {
month: month.toString()
});
return moment(temp, 'YYYY-MM');
} else if (/^\d{4}-\d{1,2}$/.test(dt)) {
return moment(dt, 'YYYY-MM');
} else if (/^\s*\d{4}\s*$/.test(dt)) {
return moment(dt, 'YYYY');
} else if (/^\s*$/.test(dt)) {
return moment();
} else {
mt = moment(dt);
if (mt.isValid()) {
return mt;
}
if (throws) {
throw 'Invalid date format encountered.';
}
return null;
}
} else {
if (!dt) {
return moment();
} else if (dt.isValid && dt.isValid()) {
return dt;
}
if (throws) {
throw 'Unknown date object encountered.';
}
return null;
}
};
}).call(this);
//# sourceMappingURL=fluent-date.js.map

510
dist/core/fresh-resume.js vendored Normal file
View File

@ -0,0 +1,510 @@
/**
Definition of the FRESHResume class.
@license MIT. See LICENSE.md for details.
@module core/fresh-resume
*/
(function() {
var AbstractResume, CONVERTER, FS, FluentDate, FreshResume, JRSResume, MD, PATH, XML, _, __, _parseDates, extend, moment, validator,
extend1 = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
FS = require('fs');
extend = require('extend');
validator = require('is-my-json-valid');
_ = require('underscore');
__ = require('lodash');
PATH = require('path');
moment = require('moment');
XML = require('xml-escape');
MD = require('marked');
CONVERTER = require('fresh-jrs-converter');
JRSResume = require('./jrs-resume');
FluentDate = require('./fluent-date');
AbstractResume = require('./abstract-resume');
/**
A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
object is an instantiation of that JSON decorated with utility methods.
@constructor
*/
FreshResume = (function(superClass) {
extend1(FreshResume, superClass);
function FreshResume() {
return FreshResume.__super__.constructor.apply(this, arguments);
}
/** Initialize the the FreshResume from JSON string data. */
FreshResume.prototype.parse = function(stringData, opts) {
var ref;
this.imp = (ref = this.imp) != null ? ref : {
raw: stringData
};
return this.parseJSON(JSON.parse(stringData), opts);
};
/**
Initialize the FreshResume from JSON.
Open and parse the specified FRESH resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
*/
FreshResume.prototype.parseJSON = function(rep, opts) {
var ignoreList, ref, scrubbed, that, traverse;
that = this;
traverse = require('traverse');
ignoreList = [];
scrubbed = traverse(rep).map(function(x) {
if (!this.isLeaf && this.node.ignore) {
if (this.node.ignore === true || this.node.ignore === 'true') {
ignoreList.push(this.node);
return this.remove();
}
}
});
extend(true, this, scrubbed);
if (!((ref = this.imp) != null ? ref.processed : void 0)) {
opts = opts || {};
if (opts.imp === void 0 || opts.imp) {
this.imp = this.imp || {};
this.imp.title = (opts.title || this.imp.title) || this.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
}
}
this.imp.processed = true;
(opts.date === void 0 || opts.date) && _parseDates.call(this);
(opts.sort === void 0 || opts.sort) && this.sort();
(opts.compute === void 0 || opts.compute) && (this.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
}
return this;
};
/** Save the sheet to disk (for environments that have disk access). */
FreshResume.prototype.save = function(filename) {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
return this;
};
/**
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
FreshResume.prototype.saveAs = function(filename, format) {
var newRep;
if (format !== 'JRS') {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
} else {
newRep = CONVERTER.toJRS(this);
FS.writeFileSync(filename, JRSResume.stringify(newRep), 'utf8');
}
return this;
};
/**
Duplicate this FreshResume instance.
This method first extend()s this object onto an empty, creating a deep copy,
and then passes the result into a new FreshResume instance via .parseJSON.
We do it this way to create a true clone of the object without re-running any
of the associated processing.
*/
FreshResume.prototype.dupe = function() {
var jso, rnew;
jso = extend(true, {}, this);
rnew = new FreshResume();
rnew.parseJSON(jso, {});
return rnew;
};
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way.
*/
FreshResume.prototype.stringify = function() {
return FreshResume.stringify(this);
};
/**
Create a copy of this resume in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder).
TODO: Move this out of FRESHResume.
*/
FreshResume.prototype.transformStrings = function(filt, transformer) {
var ret, trx;
ret = this.dupe();
trx = require('../utils/string-transformer');
return trx(ret, filt, transformer);
};
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
FreshResume.prototype.markdownify = function() {
var MDIN, trx;
MDIN = function(txt) {
return MD(txt || '').replace(/^\s*<p>|<\/p>\s*$/gi, '');
};
trx = function(key, val) {
if (key === 'summary') {
return MD(val);
}
return MDIN(val);
};
return this.transformStrings(['skills', 'url', 'start', 'end', 'date'], trx);
};
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
FreshResume.prototype.xmlify = function() {
var trx;
trx = function(key, val) {
return XML(val);
};
return this.transformStrings([], trx);
};
/** Return the resume format. */
FreshResume.prototype.format = function() {
return 'FRESH';
};
/**
Return internal metadata. Create if it doesn't exist.
*/
FreshResume.prototype.i = function() {
return this.imp = this.imp || {};
};
/** Return a unique list of all keywords across all skills. */
FreshResume.prototype.keywords = function() {
var flatSkills;
flatSkills = [];
if (this.skills) {
if (this.skills.sets) {
flatSkills = this.skills.sets.map(function(sk) {
return sk.skills;
}).reduce(function(a, b) {
return a.concat(b);
});
} else if (this.skills.list) {
flatSkills = flatSkills.concat(this.skills.list.map(function(sk) {
return sk.name;
}));
}
flatSkills = _.uniq(flatSkills);
}
return flatSkills;
};
/**
Reset the sheet to an empty state. TODO: refactor/review
*/
FreshResume.prototype.clear = function(clearMeta) {
clearMeta = ((clearMeta === void 0) && true) || clearMeta;
if (clearMeta) {
delete this.imp;
}
delete this.computed;
delete this.employment;
delete this.service;
delete this.education;
delete this.recognition;
delete this.reading;
delete this.writing;
delete this.interests;
delete this.skills;
return delete this.social;
};
/**
Get a safe count of the number of things in a section.
*/
FreshResume.prototype.count = function(obj) {
if (!obj) {
return 0;
}
if (obj.history) {
return obj.history.length;
}
if (obj.sets) {
return obj.sets.length;
}
return obj.length || 0;
};
/** Add work experience to the sheet. */
FreshResume.prototype.add = function(moniker) {
var defSheet, newObject;
defSheet = FreshResume["default"]();
newObject = defSheet[moniker].history ? $.extend(true, {}, defSheet[moniker].history[0]) : moniker === 'skills' ? $.extend(true, {}, defSheet.skills.sets[0]) : $.extend(true, {}, defSheet[moniker][0]);
this[moniker] = this[moniker] || [];
if (this[moniker].history) {
this[moniker].history.push(newObject);
} else if (moniker === 'skills') {
this.skills.sets.push(newObject);
} else {
this[moniker].push(newObject);
}
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;
});
};
/** 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. */
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 ret, schemaObj, validate;
schemaObj = require('fresca');
validator = require('is-my-json-valid');
validate = validator(schemaObj, {
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
ret = validate(this);
if (!ret) {
this.imp = this.imp || {};
this.imp.validationErrors = validate.errors;
}
return ret;
};
FreshResume.prototype.duration = function(unit) {
return FreshResume.__super__.duration.call(this, 'employment.history', 'start', 'end', unit);
};
/**
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() {
var byDateDesc, sortSection;
byDateDesc = function(a, b) {
if (a.safe.start.isBefore(b.safe.start)) {
return 1;
} else {
if (a.safe.start.isAfter(b.safe.start)) {
return -1;
} else {
return 0;
}
}
};
sortSection = function(key) {
var ar, datedThings;
ar = __.get(this, key);
if (ar && ar.length) {
datedThings = obj.filter(function(o) {
return o.start;
});
return datedThings.sort(byDateDesc);
}
};
sortSection('employment.history');
sortSection('education.history');
sortSection('service.history');
sortSection('projects');
return this.writing && this.writing.sort(function(a, b) {
if (a.safe.date.isBefore(b.safe.date)) {
return 1;
} else {
return (a.safe.date.isAfter(b.safe.date) && -1) || 0;
}
});
};
return FreshResume;
})(AbstractResume);
/**
Get the default (starter) sheet.
*/
FreshResume["default"] = function() {
return new FreshResume().parseJSON(require('fresh-resume-starter').fresh);
};
/**
Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
along the way.
*/
FreshResume.stringify = function(obj) {
var replacer;
replacer = function(key, value) {
var exKeys;
exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'];
if (_.some(exKeys, function(val) {
return key.trim() === val;
})) {
return void 0;
} else {
return value;
}
};
return JSON.stringify(obj, replacer, 2);
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
_parseDates = function() {
var _fmt, replaceDatesInObject, that;
_fmt = require('./fluent-date').fmt;
that = this;
replaceDatesInObject = function(obj) {
if (!obj) {
return;
}
if (Object.prototype.toString.call(obj) === '[object Array]') {
obj.forEach(function(elem) {
return replaceDatesInObject(elem);
});
} else if (typeof obj === 'object') {
if (obj._isAMomentObject || obj.safe) {
return;
}
Object.keys(obj).forEach(function(key) {
return replaceDatesInObject(obj[key]);
});
['start', 'end', 'date'].forEach(function(val) {
if ((obj[val] !== void 0) && (!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]);
});
};
/** Export the Sheet function/ctor. */
module.exports = FreshResume;
}).call(this);
//# sourceMappingURL=fresh-resume.js.map

230
dist/core/fresh-theme.js vendored Normal file
View File

@ -0,0 +1,230 @@
/**
Definition of the FRESHTheme class.
@module core/fresh-theme
@license MIT. See LICENSE.md for details.
*/
(function() {
var EXTEND, FRESHTheme, FS, HMSTATUS, PATH, READFILES, _, _load, _loadOne, friendlyName, loadSafeJson, moment, parsePath, pathExists, validator;
FS = require('fs');
validator = require('is-my-json-valid');
_ = require('underscore');
PATH = require('path');
parsePath = require('parse-filepath');
pathExists = require('path-exists').sync;
EXTEND = require('extend');
HMSTATUS = require('./status-codes');
moment = require('moment');
loadSafeJson = require('../utils/safe-json-loader');
READFILES = require('recursive-readdir-sync');
/* A representation of a FRESH theme asset.
@class FRESHTheme
*/
FRESHTheme = (function() {
function FRESHTheme() {}
/* Open and parse the specified theme. */
FRESHTheme.prototype.open = function(themeFolder) {
var cached, formatsHash, pathInfo, that, themeFile, themeInfo;
this.folder = themeFolder;
pathInfo = parsePath(themeFolder);
formatsHash = {};
themeFile = PATH.join(themeFolder, 'theme.json');
themeInfo = loadSafeJson(themeFile);
if (themeInfo.ex) {
throw {
fluenterror: themeInfo.ex.operation === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError,
inner: themeInfo.ex.inner
};
}
that = this;
EXTEND(true, this, themeInfo.json);
if (this.inherits) {
cached = {};
_.each(this.inherits, function(th, key) {
var d, themePath, themesFolder;
themesFolder = require.resolve('fresh-themes');
d = parsePath(themeFolder).dirname;
themePath = PATH.join(d, th);
cached[th] = cached[th] || new FRESHTheme().open(themePath);
return formatsHash[key] = cached[th].getFormat(key);
});
}
formatsHash = _load.call(this, formatsHash);
this.formats = formatsHash;
this.name = parsePath(this.folder).name;
return this;
};
/* Determine if the theme supports the specified output format. */
FRESHTheme.prototype.hasFormat = function(fmt) {
return _.has(this.formats, fmt);
};
/* Determine if the theme supports the specified output format. */
FRESHTheme.prototype.getFormat = function(fmt) {
return this.formats[fmt];
};
return FRESHTheme;
})();
/* Load and parse theme source files. */
_load = function(formatsHash) {
var copyOnly, fmts, major, that, tplFolder;
that = this;
major = false;
tplFolder = PATH.join(this.folder, 'src');
copyOnly = ['.ttf', '.otf', '.png', '.jpg', '.jpeg', '.pdf'];
fmts = READFILES(tplFolder).map(function(absPath) {
return _loadOne.call(this, absPath, formatsHash, tplFolder);
}, this);
this.cssFiles = fmts.filter(function(fmt) {
return fmt && (fmt.ext === 'css');
});
this.cssFiles.forEach(function(cssf) {
var idx;
idx = _.findIndex(fmts, function(fmt) {
return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
});
cssf.major = false;
if (idx > -1) {
fmts[idx].css = cssf.data;
return fmts[idx].cssPath = cssf.path;
} else {
if (that.inherits) {
return that.overrides = {
file: cssf.path,
data: cssf.data
};
}
}
});
return formatsHash;
};
/* Load a single theme file. */
_loadOne = function(absPath, formatsHash, tplFolder) {
var absPathSafe, act, defFormats, idx, isPrimary, obj, outFmt, pathInfo, portion, ref, ref1, reg, res;
pathInfo = parsePath(absPath);
absPathSafe = absPath.trim().toLowerCase();
outFmt = '';
act = 'copy';
isPrimary = false;
if (this.explicit) {
outFmt = _.find(Object.keys(this.formats), function(fmtKey) {
var fmtVal;
fmtVal = this.formats[fmtKey];
return _.some(fmtVal.transform, function(fpath) {
var absPathB;
absPathB = PATH.join(this.folder, fpath).trim().toLowerCase();
return absPathB === absPathSafe;
}, this);
}, this);
if (outFmt) {
act = 'transform';
}
}
if (!outFmt) {
portion = pathInfo.dirname.replace(tplFolder, '');
if (portion && portion.trim()) {
if (portion[1] === '_') {
return;
}
reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
res = reg.exec(portion);
if (res) {
if (res[1] !== 'partials') {
outFmt = res[1];
if (!this.explicit) {
act = 'transform';
}
} else {
this.partials = this.partials || [];
this.partials.push({
name: pathInfo.name,
path: absPath
});
return null;
}
}
}
}
if (!outFmt) {
idx = pathInfo.name.lastIndexOf('-');
outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx + 1);
if (!this.explicit) {
act = 'transform';
}
defFormats = require('./default-formats');
isPrimary = _.some(defFormats, function(form) {
return form.name === outFmt && pathInfo.extname !== '.css';
});
}
formatsHash[outFmt] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
};
if ((ref = this.formats) != null ? (ref1 = ref[outFmt]) != null ? ref1.symLinks : void 0 : void 0) {
formatsHash[outFmt].symLinks = this.formats[outFmt].symLinks;
}
obj = {
action: act,
primary: isPrimary,
path: absPath,
orgPath: PATH.relative(tplFolder, absPath),
ext: pathInfo.extname.slice(1),
title: friendlyName(outFmt),
pre: outFmt,
data: FS.readFileSync(absPath, 'utf8'),
css: null
};
formatsHash[outFmt].files.push(obj);
return obj;
};
/* Return a more friendly name for certain formats. */
friendlyName = function(val) {
var friendly;
val = (val && val.trim().toLowerCase()) || '';
friendly = {
yml: 'yaml',
md: 'markdown',
txt: 'text'
};
return friendly[val] || val;
};
module.exports = FRESHTheme;
}).call(this);
//# sourceMappingURL=fresh-theme.js.map

422
dist/core/jrs-resume.js vendored Normal file
View File

@ -0,0 +1,422 @@
/**
Definition of the JRSResume class.
@license MIT. See LICENSE.md for details.
@module core/jrs-resume
*/
(function() {
var AbstractResume, CONVERTER, FS, JRSResume, MD, PATH, _, _parseDates, extend, moment, validator,
extend1 = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
FS = require('fs');
extend = require('extend');
validator = require('is-my-json-valid');
_ = require('underscore');
PATH = require('path');
MD = require('marked');
CONVERTER = require('fresh-jrs-converter');
moment = require('moment');
AbstractResume = require('./abstract-resume');
/**
A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
is an instantiation of that JSON decorated with utility methods.
@class JRSResume
*/
JRSResume = (function(superClass) {
var clear;
extend1(JRSResume, superClass);
function JRSResume() {
return JRSResume.__super__.constructor.apply(this, arguments);
}
/** Initialize the the JSResume from string. */
JRSResume.prototype.parse = function(stringData, opts) {
var ref;
this.imp = (ref = this.imp) != null ? ref : {
raw: stringData
};
return this.parseJSON(JSON.parse(stringData), opts);
};
/**
Initialize the JRSResume object from JSON.
Open and parse the specified JRS resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
*/
JRSResume.prototype.parseJSON = function(rep, opts) {
var ignoreList, ref, scrubbed, that, traverse;
opts = opts || {};
that = this;
traverse = require('traverse');
ignoreList = [];
scrubbed = traverse(rep).map(function(x) {
if (!this.isLeaf && this.node.ignore) {
if (this.node.ignore === true || this.node.ignore === 'true') {
ignoreList.push(this.node);
return this.remove();
}
}
});
extend(true, this, scrubbed);
if (!((ref = this.imp) != null ? ref.processed : void 0)) {
opts = opts || {};
if (opts.imp === void 0 || opts.imp) {
this.imp = this.imp || {};
this.imp.title = (opts.title || this.imp.title) || this.basics.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
}
}
this.imp.processed = true;
}
(opts.date === void 0 || opts.date) && _parseDates.call(this);
(opts.sort === void 0 || opts.sort) && this.sort();
if (opts.compute === void 0 || opts.compute) {
this.basics.computed = {
numYears: this.duration(),
keywords: this.keywords()
};
}
return this;
};
/** Save the sheet to disk (for environments that have disk access). */
JRSResume.prototype.save = function(filename) {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(this), 'utf8');
return this;
};
/** Save the sheet to disk in a specific format, either FRESH or JRS. */
JRSResume.prototype.saveAs = function(filename, format) {
var newRep, stringRep;
if (format === 'JRS') {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
} else {
newRep = CONVERTER.toFRESH(this);
stringRep = CONVERTER.toSTRING(newRep);
FS.writeFileSync(filename, stringRep, 'utf8');
}
return this;
};
/** Return the resume format. */
JRSResume.prototype.format = function() {
return 'JRS';
};
JRSResume.prototype.stringify = function() {
return JRSResume.stringify(this);
};
/** Return a unique list of all keywords across all skills. */
JRSResume.prototype.keywords = function() {
var flatSkills;
flatSkills = [];
if (this.skills && this.skills.length) {
this.skills.forEach(function(s) {
return flatSkills = _.union(flatSkills, s.keywords);
});
}
return flatSkills;
};
/**
Return internal metadata. Create if it doesn't exist.
JSON Resume v0.0.0 doesn't allow additional properties at the root level,
so tuck this into the .basic sub-object.
*/
JRSResume.prototype.i = function() {
var ref;
return this.imp = (ref = this.imp) != null ? ref : {};
};
/** Reset the sheet to an empty state. */
clear = function(clearMeta) {
clearMeta = ((clearMeta === void 0) && true) || clearMeta;
if (clearMeta) {
delete this.imp;
}
delete this.basics.computed;
delete this.work;
delete this.volunteer;
delete this.education;
delete this.awards;
delete this.publications;
delete this.interests;
delete this.skills;
return delete this.basics.profiles;
};
/** Add work experience to the sheet. */
JRSResume.prototype.add = function(moniker) {
var defSheet, newObject;
defSheet = JRSResume["default"]();
newObject = $.extend(true, {}, defSheet[moniker][0]);
this[moniker] = this[moniker] || [];
this[moniker].push(newObject);
return newObject;
};
/** Determine if the sheet includes a specific social profile (eg, GitHub). */
JRSResume.prototype.hasProfile = function(socialNetwork) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.basics.profiles && _.some(this.basics.profiles, function(p) {
return p.network.trim().toLowerCase() === socialNetwork;
});
};
/** Determine if the sheet includes a specific 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) {
return kw.trim().toLowerCase() === skill;
});
});
};
/** Validate the sheet against the JSON Resume schema. */
JRSResume.prototype.isValid = function() {
var ret, schema, schemaObj, temp, validate;
schema = FS.readFileSync(PATH.join(__dirname, 'resume.json'), 'utf8');
schemaObj = JSON.parse(schema);
validator = require('is-my-json-valid');
validate = validator(schemaObj, {
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
temp = this.imp;
delete this.imp;
ret = validate(this);
this.imp = temp;
if (!ret) {
this.imp = this.imp || {};
this.imp.validationErrors = validate.errors;
}
return ret;
};
JRSResume.prototype.duration = function(unit) {
return JRSResume.__super__.duration.call(this, 'work', 'startDate', 'endDate', unit);
};
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
JRSResume.prototype.sort = function() {
var byDateDesc;
byDateDesc = function(a, b) {
if (a.safeStartDate.isBefore(b.safeStartDate)) {
return 1;
} else {
return (a.safeStartDate.isAfter(b.safeStartDate) && -1) || 0;
}
};
this.work && this.work.sort(byDateDesc);
this.education && this.education.sort(byDateDesc);
this.volunteer && this.volunteer.sort(byDateDesc);
this.awards && this.awards.sort(function(a, b) {
if (a.safeDate.isBefore(b.safeDate)) {
return 1;
} else {
return (a.safeDate.isAfter(b.safeDate) && -1) || 0;
}
});
return this.publications && this.publications.sort(function(a, b) {
if (a.safeReleaseDate.isBefore(b.safeReleaseDate)) {
return 1;
} else {
return (a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1) || 0;
}
});
};
JRSResume.prototype.dupe = function() {
var rnew;
rnew = new JRSResume();
rnew.parse(this.stringify(), {});
return rnew;
};
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
JRSResume.prototype.harden = function() {
var HD, HDIN, hardenStringsInObject, ret, that;
that = this;
ret = this.dupe();
HD = function(txt) {
return '@@@@~' + txt + '~@@@@';
};
HDIN = function(txt) {
return HD(txt);
};
hardenStringsInObject = function(obj, inline) {
if (!obj) {
return;
}
inline = inline === void 0 || inline;
if (Object.prototype.toString.call(obj) === '[object Array]') {
return obj.forEach(function(elem, idx, ar) {
if (typeof elem === 'string' || elem instanceof String) {
return ar[idx] = inline ? HDIN(elem) : HD(elem);
} else {
return hardenStringsInObject(elem);
}
});
} else if (typeof obj === 'object') {
return Object.keys(obj).forEach(function(key) {
var sub;
sub = obj[key];
if (typeof sub === 'string' || sub instanceof String) {
if (_.contains(['skills', 'url', 'website', 'startDate', 'endDate', 'releaseDate', 'date', 'phone', 'email', 'address', 'postalCode', 'city', 'country', 'region'], key)) {
return;
}
if (key === 'summary') {
return obj[key] = HD(obj[key]);
} else {
return obj[key] = inline ? HDIN(obj[key]) : HD(obj[key]);
}
} else {
return hardenStringsInObject(sub);
}
});
}
};
Object.keys(ret).forEach(function(member) {
return hardenStringsInObject(ret[member]);
});
return ret;
};
return JRSResume;
})(AbstractResume);
/** Get the default (empty) sheet. */
JRSResume["default"] = function() {
return new JRSResume().parseJSON(require('fresh-resume-starter').jrs);
};
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
JRSResume.stringify = function(obj) {
var replacer;
replacer = function(key, value) {
var temp;
temp = _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'], function(val) {
return key.trim() === val;
});
if (temp) {
return void 0;
} else {
return value;
}
};
return JSON.stringify(obj, replacer, 2);
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
_parseDates = function() {
var _fmt;
_fmt = require('./fluent-date').fmt;
this.work && this.work.forEach(function(job) {
job.safeStartDate = _fmt(job.startDate);
return job.safeEndDate = _fmt(job.endDate);
});
this.education && this.education.forEach(function(edu) {
edu.safeStartDate = _fmt(edu.startDate);
return edu.safeEndDate = _fmt(edu.endDate);
});
this.volunteer && this.volunteer.forEach(function(vol) {
vol.safeStartDate = _fmt(vol.startDate);
return vol.safeEndDate = _fmt(vol.endDate);
});
this.awards && this.awards.forEach(function(awd) {
return awd.safeDate = _fmt(awd.date);
});
return this.publications && this.publications.forEach(function(pub) {
return pub.safeReleaseDate = _fmt(pub.releaseDate);
});
};
/**
Export the JRSResume function/ctor.
*/
module.exports = JRSResume;
}).call(this);
//# sourceMappingURL=jrs-resume.js.map

107
dist/core/jrs-theme.js vendored Normal file
View File

@ -0,0 +1,107 @@
/**
Definition of the JRSTheme class.
@module core/jrs-theme
@license MIT. See LICENSE.MD for details.
*/
(function() {
var JRSTheme, PATH, _, parsePath, pathExists;
_ = require('underscore');
PATH = require('path');
parsePath = require('parse-filepath');
pathExists = require('path-exists').sync;
/**
The JRSTheme class is a representation of a JSON Resume theme asset.
@class JRSTheme
*/
JRSTheme = (function() {
function JRSTheme() {}
/**
Open and parse the specified theme.
@method open
*/
JRSTheme.prototype.open = function(thFolder) {
var pathInfo, pkgJsonPath, thApi, thPkg;
this.folder = thFolder;
pathInfo = parsePath(thFolder);
pkgJsonPath = PATH.join(thFolder, 'package.json');
if (pathExists(pkgJsonPath)) {
thApi = require(thFolder);
thPkg = require(pkgJsonPath);
this.name = thPkg.name;
this.render = (thApi && thApi.render) || void 0;
this.engine = 'jrs';
this.formats = {
html: {
outFormat: 'html',
files: [
{
action: 'transform',
render: this.render,
primary: true,
ext: 'html',
css: null
}
]
},
pdf: {
outFormat: 'pdf',
files: [
{
action: 'transform',
render: this.render,
primary: true,
ext: 'pdf',
css: null
}
]
}
};
} else {
throw {
fluenterror: HACKMYSTATUS.missingPackageJSON
};
}
return this;
};
/**
Determine if the theme supports the output format.
@method hasFormat
*/
JRSTheme.prototype.hasFormat = function(fmt) {
return _.has(this.formats, fmt);
};
/**
Return the requested output format.
@method getFormat
*/
JRSTheme.prototype.getFormat = function(fmt) {
return this.formats[fmt];
};
return JRSTheme;
})();
module.exports = JRSTheme;
}).call(this);
//# sourceMappingURL=jrs-theme.js.map

122
dist/core/resume-factory.js vendored Normal file
View File

@ -0,0 +1,122 @@
/**
Definition of the ResumeFactory class.
@license MIT. See LICENSE.md for details.
@module core/resume-factory
*/
(function() {
var FS, HACKMYSTATUS, HME, ResumeConverter, ResumeFactory, SyntaxErrorEx, _, _parse, chalk;
FS = require('fs');
HACKMYSTATUS = require('./status-codes');
HME = require('./event-codes');
ResumeConverter = require('fresh-jrs-converter');
chalk = require('chalk');
SyntaxErrorEx = require('../utils/syntax-error-ex');
_ = require('underscore');
require('string.prototype.startswith');
/**
A simple factory class for FRESH and JSON Resumes.
@class ResumeFactory
*/
ResumeFactory = module.exports = {
/**
Load one or more resumes from disk.
@param {Object} opts An options object with settings for the factory as well
as passthrough settings for FRESHResume or JRSResume. Structure:
{
format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null)
objectify: true, // FRESH/JRSResume or raw JSON?
inner: { // Passthru options for FRESH/JRSResume
sort: false
}
}
*/
load: function(sources, opts, emitter) {
return sources.map(function(src) {
return this.loadOne(src, opts, emitter);
}, this);
},
/** Load a single resume from disk. */
loadOne: function(src, opts, emitter) {
var ResumeClass, info, isFRESH, json, objectify, orgFormat, rez, toFormat;
toFormat = opts.format;
objectify = opts.objectify;
toFormat && (toFormat = toFormat.toLowerCase().trim());
info = _parse(src, opts, emitter);
if (info.fluenterror) {
return info;
}
json = info.json;
isFRESH = json.meta && json.meta.format && json.meta.format.startsWith('FRESH@');
orgFormat = isFRESH ? 'fresh' : 'jrs';
if (toFormat && (orgFormat !== toFormat)) {
json = ResumeConverter['to' + toFormat.toUpperCase()](json);
}
rez = null;
if (objectify) {
ResumeClass = require('../core/' + (toFormat || orgFormat) + '-resume');
rez = new ResumeClass().parseJSON(json, opts.inner);
rez.i().file = src;
}
return {
file: src,
json: info.json,
rez: rez
};
}
};
_parse = function(fileName, opts, eve) {
var orgFormat, rawData, ret;
rawData = null;
try {
eve && eve.stat(HME.beforeRead, {
file: fileName
});
rawData = FS.readFileSync(fileName, 'utf8');
eve && eve.stat(HME.afterRead, {
file: fileName,
data: rawData
});
eve && eve.stat(HME.beforeParse, {
data: rawData
});
ret = {
json: JSON.parse(rawData)
};
orgFormat = ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@') ? 'fresh' : 'jrs';
eve && eve.stat(HME.afterParse, {
file: fileName,
data: ret.json,
fmt: orgFormat
});
return ret;
} catch (_error) {
return {
fluenterror: rawData ? HACKMYSTATUS.parseError : HACKMYSTATUS.readError,
inner: _error,
raw: rawData,
file: fileName
};
}
};
}).call(this);
//# sourceMappingURL=resume-factory.js.map

380
dist/core/resume.json vendored Normal file
View File

@ -0,0 +1,380 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Resume Schema",
"type": "object",
"additionalProperties": false,
"properties": {
"basics": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string"
},
"label": {
"type": "string",
"description": "e.g. Web Developer"
},
"picture": {
"type": "string",
"description": "URL (as per RFC 3986) to a picture in JPEG or PNG format"
},
"email": {
"type": "string",
"description": "e.g. thomas@gmail.com",
"format": "email"
},
"phone": {
"type": "string",
"description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923"
},
"website": {
"type": "string",
"description": "URL (as per RFC 3986) to your website, e.g. personal homepage",
"format": "uri"
},
"summary": {
"type": "string",
"description": "Write a short 2-3 sentence biography about yourself"
},
"location": {
"type": "object",
"additionalProperties": true,
"properties": {
"address": {
"type": "string",
"description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li."
},
"postalCode": {
"type": "string"
},
"city": {
"type": "string"
},
"countryCode": {
"type": "string",
"description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN"
},
"region": {
"type": "string",
"description": "The general region where you live. Can be a US state, or a province, for instance."
}
}
},
"profiles": {
"type": "array",
"description": "Specify any number of social networks that you participate in",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"network": {
"type": "string",
"description": "e.g. Facebook or Twitter"
},
"username": {
"type": "string",
"description": "e.g. neutralthoughts"
},
"url": {
"type": "string",
"description": "e.g. http://twitter.com/neutralthoughts"
}
}
}
}
}
},
"work": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"company": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"volunteer": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"organization": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"education": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"institution": {
"type": "string",
"description": "e.g. Massachusetts Institute of Technology"
},
"area": {
"type": "string",
"description": "e.g. Arts"
},
"studyType": {
"type": "string",
"description": "e.g. Bachelor"
},
"startDate": {
"type": "string",
"description": "e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"gpa": {
"type": "string",
"description": "grade point average, e.g. 3.67/4.0"
},
"courses": {
"type": "array",
"description": "List notable courses/subjects",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. H1302 - Introduction to American history"
}
}
}
}
},
"awards": {
"type": "array",
"description": "Specify any awards you have received throughout your professional career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"title": {
"type": "string",
"description": "e.g. One of the 100 greatest minds of the century"
},
"date": {
"type": "string",
"description": "e.g. 1989-06-12",
"format": "date"
},
"awarder": {
"type": "string",
"description": "e.g. Time Magazine"
},
"summary": {
"type": "string",
"description": "e.g. Received for my work with Quantum Physics"
}
}
}
},
"publications": {
"type": "array",
"description": "Specify your publications through your career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. The World Wide Web"
},
"publisher": {
"type": "string",
"description": "e.g. IEEE, Computer Magazine"
},
"releaseDate": {
"type": "string",
"description": "e.g. 1990-08-01"
},
"website": {
"type": "string",
"description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html"
},
"summary": {
"type": "string",
"description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML."
}
}
}
},
"skills": {
"type": "array",
"description": "List out your professional skill-set",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Web Development"
},
"level": {
"type": "string",
"description": "e.g. Master"
},
"keywords": {
"type": "array",
"description": "List some keywords pertaining to this skill",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. HTML"
}
}
}
}
},
"languages": {
"type": "array",
"description": "List any other languages you speak",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"language": {
"type": "string",
"description": "e.g. English, Spanish"
},
"fluency": {
"type": "string",
"description": "e.g. Fluent, Beginner"
}
}
}
},
"interests": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Philosophy"
},
"keywords": {
"type": "array",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Friedrich Nietzsche"
}
}
}
}
},
"references": {
"type": "array",
"description": "List references you have received",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Timothy Cook"
},
"reference": {
"type": "string",
"description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing."
}
}
}
}
}
}

41
dist/core/status-codes.js vendored Normal file
View File

@ -0,0 +1,41 @@
/**
Status codes for HackMyResume.
@module core/status-codes
@license MIT. See LICENSE.MD for details.
*/
(function() {
module.exports = {
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3,
missingCommand: 4,
invalidCommand: 5,
resumeNotFoundAlt: 6,
inputOutputParity: 7,
createNameMissing: 8,
pdfGeneration: 9,
missingPackageJSON: 10,
invalid: 11,
invalidFormat: 12,
notOnPath: 13,
readError: 14,
parseError: 15,
fileSaveError: 16,
generateError: 17,
invalidHelperUse: 18,
mixedMerge: 19,
invokeTemplate: 20,
compileTemplate: 21,
themeLoad: 22,
invalidParamCount: 23,
missingParam: 24,
createError: 25,
validateError: 26
};
}).call(this);
//# sourceMappingURL=status-codes.js.map

40
dist/generators/base-generator.js vendored Normal file
View File

@ -0,0 +1,40 @@
/**
Definition of the BaseGenerator class.
@module generators/base-generator
@license MIT. See LICENSE.md for details.
*/
/**
The BaseGenerator class is the root of the generator hierarchy. Functionality
common to ALL generators lives here.
*/
(function() {
var BaseGenerator;
module.exports = BaseGenerator = (function() {
/** Base-class initialize. */
function BaseGenerator(format) {
this.format = format;
}
/** Status codes. */
BaseGenerator.prototype.codes = require('../core/status-codes');
/** Generator options. */
BaseGenerator.prototype.opts = {};
return BaseGenerator;
})();
}).call(this);
//# sourceMappingURL=base-generator.js.map

53
dist/generators/html-generator.js vendored Normal file
View File

@ -0,0 +1,53 @@
/**
Definition of the HTMLGenerator class.
@module generators/html-generator
@license MIT. See LICENSE.md for details.
*/
(function() {
var FS, HTML, HtmlGenerator, PATH, TemplateGenerator,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
TemplateGenerator = require('./template-generator');
FS = require('fs-extra');
HTML = require('html');
PATH = require('path');
require('string.prototype.endswith');
module.exports = HtmlGenerator = (function(superClass) {
extend(HtmlGenerator, superClass);
function HtmlGenerator() {
HtmlGenerator.__super__.constructor.call(this, 'html');
}
/**
Copy satellite CSS files to the destination and optionally pretty-print
the HTML resume prior to saving.
*/
HtmlGenerator.prototype.onBeforeSave = function(info) {
if (info.outputFile.endsWith('.css')) {
return info.mk;
}
if (this.opts.prettify) {
return HTML.prettyPrint(info.mk, this.opts.prettify);
} else {
return info.mk;
}
};
return HtmlGenerator;
})(TemplateGenerator);
}).call(this);
//# sourceMappingURL=html-generator.js.map

View File

@ -0,0 +1,117 @@
/**
Definition of the HtmlPdfCLIGenerator class.
@module generators/html-pdf-generator.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var FS, HMSTATUS, HtmlPdfCLIGenerator, PATH, SLASH, SPAWN, TemplateGenerator, _, engines,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
TemplateGenerator = require('./template-generator');
FS = require('fs-extra');
PATH = require('path');
SLASH = require('slash');
_ = require('underscore');
HMSTATUS = require('../core/status-codes');
SPAWN = require('../utils/safe-spawn');
/**
An HTML-driven PDF resume generator for HackMyResume. Talks to Phantom,
wkhtmltopdf, and other PDF engines over a CLI (command-line interface).
If an engine isn't installed for a particular platform, error out gracefully.
*/
module.exports = HtmlPdfCLIGenerator = (function(superClass) {
extend(HtmlPdfCLIGenerator, superClass);
function HtmlPdfCLIGenerator() {
HtmlPdfCLIGenerator.__super__.constructor.call(this, 'pdf', 'html');
}
/** Generate the binary PDF. */
HtmlPdfCLIGenerator.prototype.onBeforeSave = function(info) {
var safe_eng;
if (info.ext !== 'html' && info.ext !== 'pdf') {
return info.mk;
}
safe_eng = info.opts.pdf || 'wkhtmltopdf';
if (safe_eng === 'phantom') {
safe_eng = 'phantomjs';
}
if (_.has(engines, safe_eng)) {
this.errHandler = info.opts.errHandler;
engines[safe_eng].call(this, info.mk, info.outputFile, this.onError);
return null;
}
};
/* Low-level error callback for spawn(). May be called after HMR process
termination, so object references may not be valid here. That's okay; if
the references are invalid, the error was already logged. We could use
spawn-watch here but that causes issues on legacy Node.js.
*/
HtmlPdfCLIGenerator.prototype.onError = function(ex, param) {
var ref;
if ((ref = param.errHandler) != null) {
if (typeof ref.err === "function") {
ref.err(HMSTATUS.pdfGeneration, ex);
}
}
};
return HtmlPdfCLIGenerator;
})(TemplateGenerator);
engines = {
/**
Generate a PDF from HTML using wkhtmltopdf's CLI interface.
Spawns a child process with `wkhtmltopdf <source> <target>`. wkhtmltopdf
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease wkhtmltopdf rendering
*/
wkhtmltopdf: function(markup, fOut, on_error) {
var tempFile;
tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
SPAWN('wkhtmltopdf', [tempFile, fOut], false, on_error, this);
},
/**
Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
*/
phantomjs: function(markup, fOut, on_error) {
var destPath, scriptPath, sourcePath, tempFile;
tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
scriptPath = PATH.relative(process.cwd(), PATH.resolve(__dirname, '../utils/rasterize.js'));
scriptPath = SLASH(scriptPath);
sourcePath = SLASH(PATH.relative(process.cwd(), tempFile));
destPath = SLASH(PATH.relative(process.cwd(), fOut));
SPAWN('phantomjs', [scriptPath, sourcePath, destPath], false, on_error, this);
}
};
}).call(this);
//# sourceMappingURL=html-pdf-cli-generator.js.map

75
dist/generators/html-png-generator.js vendored Normal file
View File

@ -0,0 +1,75 @@
/**
Definition of the HtmlPngGenerator class.
@module generators/html-png-generator
@license MIT. See LICENSE.MD for details.
*/
(function() {
var FS, HTML, HtmlPngGenerator, PATH, SLASH, SPAWN, TemplateGenerator, phantom,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
TemplateGenerator = require('./template-generator');
FS = require('fs-extra');
HTML = require('html');
SLASH = require('slash');
SPAWN = require('../utils/safe-spawn');
PATH = require('path');
/**
An HTML-based PNG resume generator for HackMyResume.
*/
module.exports = HtmlPngGenerator = (function(superClass) {
extend(HtmlPngGenerator, superClass);
function HtmlPngGenerator() {
HtmlPngGenerator.__super__.constructor.call(this, 'png', 'html');
}
HtmlPngGenerator.prototype.invoke = function(rez, themeMarkup, cssInfo, opts) {};
HtmlPngGenerator.prototype.generate = function(rez, f, opts) {
var htmlFile, htmlResults;
htmlResults = opts.targets.filter(function(t) {
return t.fmt.outFormat === 'html';
});
htmlFile = htmlResults[0].final.files.filter(function(fl) {
return fl.info.ext === 'html';
});
phantom(htmlFile[0].data, f);
};
return HtmlPngGenerator;
})(TemplateGenerator);
/**
Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
*/
phantom = function(markup, fOut) {
var destPath, info, scriptPath, sourcePath, tempFile;
tempFile = fOut.replace(/\.png$/i, '.png.html');
FS.writeFileSync(tempFile, markup, 'utf8');
scriptPath = SLASH(PATH.relative(process.cwd(), PATH.resolve(__dirname, '../utils/rasterize.js')));
sourcePath = SLASH(PATH.relative(process.cwd(), tempFile));
destPath = SLASH(PATH.relative(process.cwd(), fOut));
info = SPAWN('phantomjs', [scriptPath, sourcePath, destPath]);
};
}).call(this);
//# sourceMappingURL=html-png-generator.js.map

47
dist/generators/json-generator.js vendored Normal file
View File

@ -0,0 +1,47 @@
/**
Definition of the JsonGenerator class.
@module generators/json-generator
@license MIT. See LICENSE.md for details.
*/
(function() {
var BaseGenerator, FJCV, FS, JsonGenerator, _,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
BaseGenerator = require('./base-generator');
FS = require('fs');
_ = require('underscore');
FJCV = require('fresh-jrs-converter');
/** The JsonGenerator generates a FRESH or JRS resume as an output. */
module.exports = JsonGenerator = (function(superClass) {
extend(JsonGenerator, superClass);
function JsonGenerator() {
JsonGenerator.__super__.constructor.call(this, 'json');
}
JsonGenerator.prototype.invoke = function(rez) {
var altRez;
altRez = FJCV['to' + (rez.format() === 'FRESH' ? 'JRS' : 'FRESH')](rez);
return altRez = FJCV.toSTRING(altRez);
};
JsonGenerator.prototype.generate = function(rez, f) {
FS.writeFileSync(f, this.invoke(rez), 'utf8');
};
return JsonGenerator;
})(BaseGenerator);
}).call(this);
//# sourceMappingURL=json-generator.js.map

50
dist/generators/json-yaml-generator.js vendored Normal file
View File

@ -0,0 +1,50 @@
/**
Definition of the JsonYamlGenerator class.
@module generators/json-yaml-generator
@license MIT. See LICENSE.md for details.
*/
(function() {
var BaseGenerator, FS, JsonYamlGenerator, YAML,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
BaseGenerator = require('./base-generator');
FS = require('fs');
YAML = require('yamljs');
/**
JsonYamlGenerator takes a JSON resume object and translates it directly to
JSON without a template, producing an equivalent YAML-formatted resume. See
also YamlGenerator (yaml-generator.js).
*/
module.exports = JsonYamlGenerator = (function(superClass) {
extend(JsonYamlGenerator, superClass);
function JsonYamlGenerator() {
JsonYamlGenerator.__super__.constructor.call(this, 'yml');
}
JsonYamlGenerator.prototype.invoke = function(rez, themeMarkup, cssInfo, opts) {
return YAML.stringify(JSON.parse(rez.stringify()), Infinity, 2);
};
JsonYamlGenerator.prototype.generate = function(rez, f, opts) {
var data;
data = YAML.stringify(JSON.parse(rez.stringify()), Infinity, 2);
FS.writeFileSync(f, data, 'utf8');
return data;
};
return JsonYamlGenerator;
})(BaseGenerator);
}).call(this);
//# sourceMappingURL=json-yaml-generator.js.map

33
dist/generators/latex-generator.js vendored Normal file
View File

@ -0,0 +1,33 @@
/**
Definition of the LaTeXGenerator class.
@module generators/latex-generator
@license MIT. See LICENSE.md for details.
*/
(function() {
var LaTeXGenerator, TemplateGenerator,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
TemplateGenerator = require('./template-generator');
/**
LaTeXGenerator generates a LaTeX resume via TemplateGenerator.
*/
module.exports = LaTeXGenerator = (function(superClass) {
extend(LaTeXGenerator, superClass);
function LaTeXGenerator() {
LaTeXGenerator.__super__.constructor.call(this, 'latex', 'tex');
}
return LaTeXGenerator;
})(TemplateGenerator);
}).call(this);
//# sourceMappingURL=latex-generator.js.map

33
dist/generators/markdown-generator.js vendored Normal file
View File

@ -0,0 +1,33 @@
/**
Definition of the MarkdownGenerator class.
@module generators/markdown-generator
@license MIT. See LICENSE.md for details.
*/
(function() {
var MarkdownGenerator, TemplateGenerator,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
TemplateGenerator = require('./template-generator');
/**
MarkdownGenerator generates a Markdown-formatted resume via TemplateGenerator.
*/
module.exports = MarkdownGenerator = (function(superClass) {
extend(MarkdownGenerator, superClass);
function MarkdownGenerator() {
MarkdownGenerator.__super__.constructor.call(this, 'md', 'txt');
}
return MarkdownGenerator;
})(TemplateGenerator);
}).call(this);
//# sourceMappingURL=markdown-generator.js.map

288
dist/generators/template-generator.js vendored Normal file
View File

@ -0,0 +1,288 @@
/**
Definition of the TemplateGenerator class. TODO: Refactor
@module generators/template-generator
@license MIT. See LICENSE.md for details.
*/
(function() {
var BaseGenerator, EXTEND, FRESHTheme, FS, JRSTheme, MD, MKDIRP, PATH, TemplateGenerator, XML, _, _defaultOpts, _reg, createSymLinks, freeze, parsePath, unfreeze,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
FS = require('fs-extra');
_ = require('underscore');
MD = require('marked');
XML = require('xml-escape');
PATH = require('path');
parsePath = require('parse-filepath');
MKDIRP = require('mkdirp');
BaseGenerator = require('./base-generator');
EXTEND = require('extend');
FRESHTheme = require('../core/fresh-theme');
JRSTheme = require('../core/jrs-theme');
/**
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
*/
module.exports = TemplateGenerator = (function(superClass) {
extend(TemplateGenerator, superClass);
/** Constructor. Set the output format and template format for this
generator. Will usually be called by a derived generator such as
HTMLGenerator or MarkdownGenerator.
*/
function TemplateGenerator(outputFormat, templateFormat, cssFile) {
TemplateGenerator.__super__.constructor.call(this, outputFormat);
this.tplFormat = templateFormat || outputFormat;
return;
}
/** Generate a resume using string-based inputs and outputs without touching
the filesystem.
@method invoke
@param rez A FreshResume object.
@param opts Generator options.
@returns {Array} An array of objects representing the generated output
files.
*/
TemplateGenerator.prototype.invoke = function(rez, opts) {
var curFmt, results;
opts = opts ? (this.opts = EXTEND(true, {}, _defaultOpts, opts)) : this.opts;
curFmt = opts.themeObj.getFormat(this.format);
curFmt.files = _.sortBy(curFmt.files, function(fi) {
return fi.ext !== 'css';
});
results = curFmt.files.map(function(tplInfo, idx) {
var trx;
if (tplInfo.action === 'transform') {
trx = this.transform(rez, tplInfo.data, this.format, opts, opts.themeObj, curFmt);
if (tplInfo.ext === 'css') {
curFmt.files[idx].data = trx;
} else {
tplInfo.ext === 'html';
}
} else {
}
if (typeof opts.onTransform === "function") {
opts.onTransform(tplInfo);
}
return {
info: tplInfo,
data: trx
};
}, this);
return {
files: results
};
};
/** Generate a resume using file-based inputs and outputs. Requires access
to the local filesystem.
@method generate
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options.
*/
TemplateGenerator.prototype.generate = function(rez, f, opts) {
var curFmt, genInfo, outFolder;
this.opts = EXTEND(true, {}, _defaultOpts, opts);
genInfo = this.invoke(rez, null);
outFolder = parsePath(f).dirname;
curFmt = opts.themeObj.getFormat(this.format);
genInfo.files.forEach(function(file) {
var thisFilePath;
file.info.orgPath = file.info.orgPath || '';
thisFilePath = file.info.primary ? f : PATH.join(outFolder, file.info.orgPath);
if (file.info.action !== 'copy' && this.onBeforeSave) {
file.data = this.onBeforeSave({
theme: opts.themeObj,
outputFile: thisFilePath,
mk: file.data,
opts: this.opts,
ext: file.info.ext
});
if (!file.data) {
return;
}
}
if (typeof opts.beforeWrite === "function") {
opts.beforeWrite(thisFilePath);
}
MKDIRP.sync(PATH.dirname(thisFilePath));
if (file.info.action !== 'copy') {
FS.writeFileSync(thisFilePath, file.data, {
encoding: 'utf8',
flags: 'w'
});
} else {
FS.copySync(file.info.path, thisFilePath);
}
if (typeof opts.afterWrite === "function") {
opts.afterWrite(thisFilePath);
}
if (this.onAfterSave) {
return this.onAfterSave({
outputFile: fileName,
mk: file.data,
opts: this.opts
});
}
}, this);
createSymLinks(curFmt, outFolder);
return genInfo;
};
/** Perform a single resume resume transformation using string-based inputs
and outputs without touching the local file system.
@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.
*/
TemplateGenerator.prototype.transform = function(json, jst, format, opts, theme, curFmt) {
var eng, result;
if (this.opts.freezeBreaks) {
jst = freeze(jst);
}
eng = require('../renderers/' + theme.engine + '-generator');
result = eng.generate(json, jst, format, curFmt, opts, theme);
if (this.opts.freezeBreaks) {
result = unfreeze(result);
}
return result;
};
return TemplateGenerator;
})(BaseGenerator);
createSymLinks = function(curFmt, outFolder) {
if (curFmt.symLinks) {
Object.keys(curFmt.symLinks).forEach(function(loc) {
var absLoc, absTarg, succeeded, type;
absLoc = PATH.join(outFolder, loc);
absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
type = parsePath(absLoc).extname ? 'file' : 'junction';
try {
return FS.symlinkSync(absTarg, absLoc, type);
} catch (_error) {
succeeded = false;
if (_error.code === 'EEXIST') {
FS.unlinkSync(absLoc);
try {
FS.symlinkSync(absTarg, absLoc, type);
succeeded = true;
} catch (_error) {}
}
if (!succeeded) {
throw ex;
}
}
});
}
};
/** Freeze newlines for protection against errant JST parsers. */
freeze = function(markup) {
markup.replace(_reg.regN, _defaultOpts.nSym);
return markup.replace(_reg.regR, _defaultOpts.rSym);
};
/** Unfreeze newlines when the coast is clear. */
unfreeze = function(markup) {
markup.replace(_reg.regSymR, '\r');
return markup.replace(_reg.regSymN, '\n');
};
/** Default template generator options. */
_defaultOpts = {
engine: 'underscore',
keepBreaks: true,
freezeBreaks: false,
nSym: '&newl;',
rSym: '&retn;',
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>|<\/p>\s*$/gi, '');
},
lower: function(txt) {
return txt.toLowerCase();
},
link: function(name, url) {
if (url) {
return '<a href="' + url + '">' + name + '</a>';
} else {
return name;
}
}
},
prettify: {
indent_size: 2,
unformatted: ['em', 'strong', 'a'],
max_char: 80
}
};
/** Regexes for linebreak preservation. */
_reg = {
regN: new RegExp('\n', 'g'),
regR: new RegExp('\r', 'g'),
regSymN: new RegExp(_defaultOpts.nSym, 'g'),
regSymR: new RegExp(_defaultOpts.rSym, 'g')
};
}).call(this);
//# sourceMappingURL=template-generator.js.map

33
dist/generators/text-generator.js vendored Normal file
View File

@ -0,0 +1,33 @@
/**
Definition of the TextGenerator class.
@module generators/text-generator
@license MIT. See LICENSE.md for details.
*/
(function() {
var TemplateGenerator, TextGenerator,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
TemplateGenerator = require('./template-generator');
/**
The TextGenerator generates a plain-text resume via the TemplateGenerator.
*/
module.exports = TextGenerator = (function(superClass) {
extend(TextGenerator, superClass);
function TextGenerator() {
TextGenerator.__super__.constructor.call(this, 'txt');
}
return TextGenerator;
})(TemplateGenerator);
}).call(this);
//# sourceMappingURL=text-generator.js.map

28
dist/generators/word-generator.js vendored Normal file
View File

@ -0,0 +1,28 @@
/*
Definition of the WordGenerator class.
@module generators/word-generator
@license MIT. See LICENSE.md for details.
*/
(function() {
var TemplateGenerator, WordGenerator,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
TemplateGenerator = require('./template-generator');
module.exports = WordGenerator = (function(superClass) {
extend(WordGenerator, superClass);
function WordGenerator() {
WordGenerator.__super__.constructor.call(this, 'doc', 'xml');
}
return WordGenerator;
})(TemplateGenerator);
}).call(this);
//# sourceMappingURL=word-generator.js.map

31
dist/generators/xml-generator.js vendored Normal file
View File

@ -0,0 +1,31 @@
/**
Definition of the XMLGenerator class.
@license MIT. See LICENSE.md for details.
@module generatprs/xml-generator
*/
(function() {
var BaseGenerator, XMLGenerator,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
BaseGenerator = require('./base-generator');
/** The XmlGenerator generates an XML resume via the TemplateGenerator. */
module.exports = XMLGenerator = (function(superClass) {
extend(XMLGenerator, superClass);
function XMLGenerator() {
XMLGenerator.__super__.constructor.call(this, 'xml');
}
return XMLGenerator;
})(BaseGenerator);
}).call(this);
//# sourceMappingURL=xml-generator.js.map

33
dist/generators/yaml-generator.js vendored Normal file
View File

@ -0,0 +1,33 @@
/**
Definition of the YAMLGenerator class.
@module yaml-generator.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var TemplateGenerator, YAMLGenerator,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
TemplateGenerator = require('./template-generator');
/**
YamlGenerator generates a YAML-formatted resume via TemplateGenerator.
*/
module.exports = YAMLGenerator = (function(superClass) {
extend(YAMLGenerator, superClass);
function YAMLGenerator() {
YAMLGenerator.__super__.constructor.call(this, 'yml', 'yml');
}
return YAMLGenerator;
})(TemplateGenerator);
}).call(this);
//# sourceMappingURL=yaml-generator.js.map

71
dist/helpers/block-helpers.js vendored Normal file
View File

@ -0,0 +1,71 @@
/**
Block helper definitions for HackMyResume / FluentCV.
@license MIT. See LICENSE.md for details.
@module helpers/generic-helpers
*/
(function() {
var BlockHelpers, HMSTATUS, LO, _, unused;
HMSTATUS = require('../core/status-codes');
LO = require('lodash');
_ = require('underscore');
unused = require('../utils/string');
/** Block helper function definitions. */
BlockHelpers = module.exports = {
/**
Emit the enclosed content if the resume has a section with
the specified name. Otherwise, emit an empty string ''.
*/
section: function(title, options) {
var obj, ret;
title = title.trim().toLowerCase();
obj = LO.get(this.r, title);
ret = '';
if (obj) {
if (_.isArray(obj)) {
if (obj.length) {
ret = options.fn(this);
}
} else if (_.isObject(obj)) {
if ((obj.history && obj.history.length) || (obj.sets && obj.sets.length)) {
ret = options.fn(this);
}
}
}
return ret;
},
/**
Emit the enclosed content if the resume has the named
property or subproperty.
*/
has: function(title, options) {
title = title && title.trim().toLowerCase();
if (LO.get(this.r, title)) {
return options.fn(this);
}
},
/**
Return true if either value is truthy.
@method either
*/
either: function(lhs, rhs, options) {
if (lhs || rhs) {
return options.fn(this);
}
}
};
}).call(this);
//# sourceMappingURL=block-helpers.js.map

66
dist/helpers/console-helpers.js vendored Normal file
View File

@ -0,0 +1,66 @@
/**
Generic template helper definitions for command-line output.
@module console-helpers.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var CHALK, LO, PAD, _, consoleFormatHelpers;
PAD = require('string-padding');
LO = require('lodash');
CHALK = require('chalk');
_ = require('underscore');
require('../utils/string');
consoleFormatHelpers = module.exports = {
v: function(val, defaultVal, padding, style) {
var retVal, spaces;
retVal = val === null || val === void 0 ? defaultVal : val;
spaces = 0;
if (String.is(padding)) {
spaces = parseInt(padding, 10);
if (isNaN(spaces)) {
spaces = 0;
}
} else if (_.isNumber(padding)) {
spaces = padding;
}
if (spaces !== 0) {
retVal = PAD(retVal, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT);
}
if (style && String.is(style)) {
retVal = LO.get(CHALK, style)(retVal);
}
return retVal;
},
gapLength: function(val) {
if (val < 35) {
return CHALK.green.bold(val);
} else if (val < 95) {
return CHALK.yellow.bold(val);
} else {
return CHALK.red.bold(val);
}
},
style: function(val, style) {
return LO.get(CHALK, style)(val);
},
isPlural: function(val, options) {
if (val > 1) {
return options.fn(this);
}
},
pad: function(val, spaces) {
return PAD(val, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT);
}
};
}).call(this);
//# sourceMappingURL=console-helpers.js.map

633
dist/helpers/generic-helpers.js vendored Normal file
View File

@ -0,0 +1,633 @@
/**
Generic template helper definitions for HackMyResume / FluentCV.
@license MIT. See LICENSE.md for details.
@module helpers/generic-helpers
*/
(function() {
var FS, FluentDate, GenericHelpers, H2W, HMSTATUS, LO, MD, PATH, XML, _, _fromTo, _reportError, moment, printf, skillLevelToIndex, unused;
MD = require('marked');
H2W = require('../utils/html-to-wpml');
XML = require('xml-escape');
FluentDate = require('../core/fluent-date');
HMSTATUS = require('../core/status-codes');
moment = require('moment');
FS = require('fs');
LO = require('lodash');
PATH = require('path');
printf = require('printf');
_ = require('underscore');
unused = require('../utils/string');
/** Generic template helper function definitions. */
GenericHelpers = module.exports = {
/**
Emit a formatted string representing the specified datetime.
Convert the input date to the specified format through Moment.js. If date is
valid, return the formatted date string. If date is null, undefined, or other
falsy value, return the value of the 'fallback' parameter, if specified, or
null if no fallback was specified. If date is invalid, but not null/undefined/
falsy, return it as-is.
@param {string|Moment} datetime A date value.
@param {string} [dtFormat='YYYY-MM'] The desired datetime format. Must be a
Moment.js-compatible datetime format.
@param {string|Moment} fallback A fallback value to use if the specified date
is null, undefined, or falsy.
*/
formatDate: function(datetime, dtFormat, fallback) {
var momentDate;
if (datetime == null) {
datetime = void 0;
}
if (dtFormat == null) {
dtFormat = 'YYYY-MM';
}
if (datetime && moment.isMoment(datetime)) {
return datetime.format(dtFormat);
}
if (String.is(datetime)) {
momentDate = moment(datetime, dtFormat);
if (momentDate.isValid()) {
return momentDate.format(dtFormat);
}
momentDate = moment(datetime);
if (momentDate.isValid()) {
return momentDate.format(dtFormat);
}
}
return datetime || (typeof fallback === 'string' ? fallback : (fallback === true ? 'Present' : ''));
},
/**
Emit a formatted string representing the specified datetime.
@param {string} dateValue A raw date value from the FRESH or JRS resume.
@param {string} [dateFormat='YYYY-MM'] The desired datetime format. Must be
compatible with Moment.js datetime formats.
@param {string} [dateDefault=null] The default date value to use if the dateValue
parameter is null, undefined, or falsy.
*/
date: function(dateValue, dateFormat, dateDefault) {
var dateValueMoment, dateValueSafe, reserved;
if (!dateDefault || !String.is(dateDefault)) {
dateDefault = 'Current';
}
if (!dateFormat || !String.is(dateFormat)) {
dateFormat = 'YYYY-MM';
}
if (!dateValue || !String.is(dateValue)) {
dateValue = null;
}
if (!dateValue) {
return dateDefault;
}
reserved = ['current', 'present', 'now'];
dateValueSafe = dateValue.trim().toLowerCase();
if (_.contains(reserved, dateValueSafe)) {
return dateValue;
}
dateValueMoment = moment(dateValue, dateFormat);
if (dateValueMoment.isValid()) {
return dateValueMoment.format(dateFormat);
}
return dateValue;
},
/**
Given a resume sub-object with a start/end date, format a representation of
the date range.
*/
dateRange: function(obj, fmt, sep, fallback) {
if (!obj) {
return '';
}
return _fromTo(obj.start, obj.end, fmt, sep, fallback);
},
/**
Format a from/to date range for display.
@method toFrom
*/
fromTo: function() {
return _fromTo.apply(this, arguments);
},
/**
Return a named color value as an RRGGBB string.
@method toFrom
*/
color: function(colorName, colorDefault) {
var ret;
if (!(colorName && colorName.trim())) {
return _reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontList',
error: HMSTATUS.missingParam,
expected: 'name'
});
} else {
if (!GenericHelpers.theme.colors) {
return colorDefault;
}
ret = GenericHelpers.theme.colors[colorName];
if (!(ret && ret.trim())) {
return colorDefault;
}
return ret;
}
},
/**
Emit the size of the specified named font.
@param key {String} A named style from the "fonts" section of the theme's
theme.json file. For example: 'default' or 'heading1'.
*/
fontSize: function(key, defSize, units) {
var fontSpec, hasDef, ret;
ret = '';
hasDef = defSize && (String.is(defSize) || _.isNumber(defSize));
if (!(key && key.trim())) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontSize',
error: HMSTATUS.missingParam,
expected: 'key'
});
return ret;
} else if (GenericHelpers.theme.fonts) {
fontSpec = LO.get(GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) {
if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[key];
}
}
if (fontSpec) {
if (String.is(fontSpec)) {
} else if (_.isArray(fontSpec)) {
if (!String.is(fontSpec[0])) {
ret = fontSpec[0].size;
}
} else {
ret = fontSpec.size;
}
}
}
if (!ret) {
if (hasDef) {
ret = defSize;
} else {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontSize',
error: HMSTATUS.missingParam,
expected: 'defSize'
});
ret = '';
}
}
return ret;
},
/**
Emit the font face (such as 'Helvetica' or 'Calibri') associated with the
provided key.
@param key {String} A named style from the "fonts" section of the theme's
theme.json file. For example: 'default' or 'heading1'.
@param defFont {String} The font to use if the specified key isn't present.
Can be any valid font-face name such as 'Helvetica Neue' or 'Calibri'.
*/
fontFace: function(key, defFont) {
var fontSpec, hasDef, ret;
ret = '';
hasDef = defFont && String.is(defFont);
if (!(key && key.trim())) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontFace',
error: HMSTATUS.missingParam,
expected: 'key'
});
return ret;
} else if (GenericHelpers.theme.fonts) {
fontSpec = LO.get(GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) {
if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[key];
}
}
if (fontSpec) {
if (String.is(fontSpec)) {
ret = fontSpec;
} else if (_.isArray(fontSpec)) {
ret = String.is(fontSpec[0]) ? fontSpec[0] : fontSpec[0].name;
} else {
ret = fontSpec.name;
}
}
}
if (!(ret && ret.trim())) {
ret = defFont;
if (!hasDef) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontFace',
error: HMSTATUS.missingParam,
expected: 'defFont'
});
ret = '';
}
}
return ret;
},
/**
Emit a comma-delimited list of font names suitable associated with the
provided key.
@param key {String} A named style from the "fonts" section of the theme's
theme.json file. For example: 'default' or 'heading1'.
@param defFontList {Array} The font list to use if the specified key isn't
present. Can be an array of valid font-face name such as 'Helvetica Neue'
or 'Calibri'.
@param sep {String} The default separator to use in the rendered output.
Defaults to ", " (comma with a space).
*/
fontList: function(key, defFontList, sep) {
var fontSpec, hasDef, ret;
ret = '';
hasDef = defFontList && String.is(defFontList);
if (!(key && key.trim())) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontList',
error: HMSTATUS.missingParam,
expected: 'key'
});
} else if (GenericHelpers.theme.fonts) {
fontSpec = LO.get(GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) {
if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[key];
}
}
if (fontSpec) {
if (String.is(fontSpec)) {
ret = fontSpec;
} else if (_.isArray(fontSpec)) {
fontSpec = fontSpec.map(function(ff) {
return "'" + (String.is(ff) ? ff : ff.name) + "'";
});
ret = fontSpec.join(sep === void 0 ? ', ' : sep || '');
} else {
ret = fontSpec.name;
}
}
}
if (!(ret && ret.trim())) {
if (!hasDef) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'fontList',
error: HMSTATUS.missingParam,
expected: 'defFontList'
});
ret = '';
} else {
ret = defFontList;
}
}
return ret;
},
/**
Capitalize the first letter of the word. TODO: Rename
@method section
*/
camelCase: function(val) {
val = (val && val.trim()) || '';
if (val) {
return val.charAt(0).toUpperCase() + val.slice(1);
} else {
return val;
}
},
/**
Display a user-overridable section title for a FRESH resume theme. Use this in
lieue of hard-coding section titles.
Usage:
{{sectionTitle "sectionName"}}
{{sectionTitle "sectionName" "sectionTitle"}}
Example:
{{sectionTitle "Education"}}
{{sectionTitle "Employment" "Project History"}}
@param sect_name The name of the section being title. Must be one of the
top-level FRESH resume sections ("info", "education", "employment", etc.).
@param sect_title The theme-specified section title. May be replaced by the
user.
@method sectionTitle
*/
sectionTitle: function(sname, stitle) {
stitle = (stitle && String.is(stitle) && stitle) || sname;
return (this.opts.stitles && this.opts.stitles[sname.toLowerCase().trim()]) || stitle;
},
/** Convert inline Markdown to inline WordProcessingML. */
wpml: function(txt, inline) {
if (!txt) {
return '';
}
inline = (inline && !inline.hash) || false;
txt = XML(txt.trim());
txt = inline ? MD(txt).replace(/^\s*<p>|<\/p>\s*$/gi, '') : MD(txt);
txt = H2W(txt);
return txt;
},
/**
Emit a conditional link.
@method link
*/
link: function(text, url) {
if (url && url.trim()) {
return '<a href="' + url + '">' + text + '</a>';
} else {
return text;
}
},
/**
Return the last word of the specified text.
@method lastWord
*/
lastWord: function(txt) {
if (txt && txt.trim()) {
return _.last(txt.split(' '));
} else {
return '';
}
},
/**
Convert a skill level to an RGB color triplet. TODO: refactor
@method skillColor
@param lvl Input skill level. Skill level can be expressed as a string
("beginner", "intermediate", etc.), as an integer (1,5,etc), as a string
integer ("1", "5", etc.), or as an RRGGBB color triplet ('#C00000',
'#FFFFAA').
*/
skillColor: function(lvl) {
var idx, skillColors;
idx = skillLevelToIndex(lvl);
skillColors = (this.theme && this.theme.palette && this.theme.palette.skillLevels) || ['#FFFFFF', '#5CB85C', '#F1C40F', '#428BCA', '#C00000'];
return skillColors[idx];
},
/**
Return an appropriate height. TODO: refactor
@method lastWord
*/
skillHeight: function(lvl) {
var idx;
idx = skillLevelToIndex(lvl);
return ['38.25', '30', '16', '8', '0'][idx];
},
/**
Return all but the last word of the input text.
@method initialWords
*/
initialWords: function(txt) {
if (txt && txt.trim()) {
return _.initial(txt.split(' ')).join(' ');
} else {
return '';
}
},
/**
Trim the protocol (http or https) from a URL/
@method trimURL
*/
trimURL: function(url) {
if (url && url.trim()) {
return url.trim().replace(/^https?:\/\//i, '');
} else {
return '';
}
},
/**
Convert text to lowercase.
@method toLower
*/
toLower: function(txt) {
if (txt && txt.trim()) {
return txt.toLowerCase();
} else {
return '';
}
},
/**
Convert text to lowercase.
@method toLower
*/
toUpper: function(txt) {
if (txt && txt.trim()) {
return txt.toUpperCase();
} else {
return '';
}
},
/**
Conditional stylesheet link. Creates a link to the specified stylesheet with
<link> or embeds the styles inline with <style></style>, depending on the
theme author's and user's preferences.
@param url {String} The path to the CSS file.
@param linkage {String} The default link method. Can be either `embed` or
`link`. If omitted, defaults to `embed`. Can be overridden by the `--css`
command-line switch.
*/
styleSheet: function(url, linkage) {
var rawCss, renderedCss, ret;
linkage = this.opts.css || linkage || 'embed';
ret = '';
if (linkage === 'link') {
ret = printf('<link href="%s" rel="stylesheet" type="text/css">', url);
} else {
rawCss = FS.readFileSync(PATH.join(this.opts.themeObj.folder, '/src/', url), 'utf8');
renderedCss = this.engine.generateSimple(this, rawCss);
ret = printf('<style>%s</style>', renderedCss);
}
if (this.opts.themeObj.inherits && this.opts.themeObj.inherits.html && this.format === 'html') {
ret += linkage === 'link' ? '<link href="' + this.opts.themeObj.overrides.path + '" rel="stylesheet" type="text/css">' : '<style>' + this.opts.themeObj.overrides.data + '</style>';
}
return ret;
},
/**
Perform a generic comparison.
See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates
@method compare
*/
compare: function(lvalue, rvalue, options) {
var operator, operators, result;
if (arguments.length < 3) {
throw new Error("Template helper 'compare' needs 2 parameters");
}
operator = options.hash.operator || "==";
operators = {
'==': function(l, r) {
return l === r;
},
'===': function(l, r) {
return l === r;
},
'!=': function(l, r) {
return l !== r;
},
'<': function(l, r) {
return l < r;
},
'>': function(l, r) {
return l > r;
},
'<=': function(l, r) {
return l <= r;
},
'>=': function(l, r) {
return l >= r;
},
'typeof': function(l, r) {
return typeof l === r;
}
};
if (!operators[operator]) {
throw new Error("Helper 'compare' doesn't know the operator " + operator);
}
result = operators[operator](lvalue, rvalue);
if (result) {
return options.fn(this);
} else {
return options.inverse(this);
}
},
pad: function(stringOrArray, padAmount, unused) {
var PAD, ret;
stringOrArray = stringOrArray || '';
padAmount = padAmount || 0;
ret = '';
PAD = require('string-padding');
if (!String.is(stringOrArray)) {
ret = stringOrArray.map(function(line) {
return PAD(line, line.length + Math.abs(padAmount), null, padAmount < 0 ? PAD.LEFT : PAD.RIGHT);
}).join('\n');
} else {
ret = PAD(stringOrArray, stringOrArray.length + Math.abs(padAmount), null, padAmount < 0 ? PAD.LEFT : PAD.RIGHT);
}
return ret;
}
};
/**
Report an error to the outside world without throwing an exception. Currently
relies on kludging the running verb into. opts.
*/
_reportError = function(code, params) {
return GenericHelpers.opts.errHandler.err(code, params);
};
/**
Format a from/to date range for display.
*/
_fromTo = function(dateA, dateB, fmt, sep, fallback) {
var dateATrim, dateBTrim, dateFrom, dateTemp, dateTo, reserved;
if (moment.isMoment(dateA) || moment.isMoment(dateB)) {
_reportError(HMSTATUS.invalidHelperUse, {
helper: 'dateRange'
});
return '';
}
dateFrom = null;
dateTo = null;
dateTemp = null;
dateA = dateA || '';
dateB = dateB || '';
dateATrim = dateA.trim().toLowerCase();
dateBTrim = dateB.trim().toLowerCase();
reserved = ['current', 'present', 'now', ''];
fmt = (fmt && String.is(fmt) && fmt) || 'YYYY-MM';
sep = (sep && String.is(sep) && sep) || ' — ';
if (_.contains(reserved, dateATrim)) {
dateFrom = fallback || '???';
} else {
dateTemp = FluentDate.fmt(dateA);
dateFrom = dateTemp.format(fmt);
}
if (_.contains(reserved, dateBTrim)) {
dateTo = fallback || 'Present';
} else {
dateTemp = FluentDate.fmt(dateB);
dateTo = dateTemp.format(fmt);
}
if (dateFrom && dateTo) {
return dateFrom + sep + dateTo;
} else if (dateFrom || dateTo) {
return dateFrom || dateTo;
}
return '';
};
skillLevelToIndex = function(lvl) {
var idx, intVal;
idx = 0;
if (String.is(lvl)) {
lvl = lvl.trim().toLowerCase();
intVal = parseInt(lvl);
if (isNaN(intVal)) {
switch (lvl) {
case 'beginner':
idx = 1;
break;
case 'intermediate':
idx = 2;
break;
case 'advanced':
idx = 3;
break;
case 'master':
idx = 4;
}
} else {
idx = Math.min(intVal / 2, 4);
idx = Math.max(0, idx);
}
} else {
idx = Math.min(lvl / 2, 4);
idx = Math.max(0, idx);
}
return idx;
};
}).call(this);
//# sourceMappingURL=generic-helpers.js.map

48
dist/helpers/handlebars-helpers.js vendored Normal file
View File

@ -0,0 +1,48 @@
/**
Template helper definitions for Handlebars.
@license MIT. See LICENSE.md for details.
@module handlebars-helpers.js
*/
(function() {
var HANDLEBARS, _, blockHelpers, helpers;
HANDLEBARS = require('handlebars');
_ = require('underscore');
helpers = require('./generic-helpers');
blockHelpers = require('./block-helpers');
/**
Register useful Handlebars helpers.
@method registerHelpers
*/
module.exports = function(theme, opts) {
var wrappedHelpers;
helpers.theme = theme;
helpers.opts = opts;
helpers.type = 'handlebars';
wrappedHelpers = _.mapObject(helpers, function(hVal, hKey) {
if (_.isFunction(hVal)) {
_.wrap(hVal, function(func) {
var args;
args = Array.prototype.slice.call(arguments);
args.shift();
args.pop();
return func.apply(this, args);
});
}
return hVal;
}, this);
HANDLEBARS.registerHelper(wrappedHelpers);
HANDLEBARS.registerHelper(blockHelpers);
};
}).call(this);
//# sourceMappingURL=handlebars-helpers.js.map

38
dist/helpers/underscore-helpers.js vendored Normal file
View File

@ -0,0 +1,38 @@
/**
Template helper definitions for Underscore.
@license MIT. See LICENSE.md for details.
@module handlebars-helpers.js
*/
(function() {
var HANDLEBARS, _, helpers;
HANDLEBARS = require('handlebars');
_ = require('underscore');
helpers = require('./generic-helpers');
/**
Register useful Underscore helpers.
@method registerHelpers
*/
module.exports = function(theme, opts, cssInfo, ctx, eng) {
helpers.theme = theme;
helpers.opts = opts;
helpers.cssInfo = cssInfo;
helpers.engine = eng;
ctx.h = helpers;
_.each(helpers, function(hVal, hKey) {
if (_.isFunction(hVal)) {
return _.bind(hVal, ctx);
}
}, this);
};
}).call(this);
//# sourceMappingURL=underscore-helpers.js.map

48
dist/index.js vendored Normal file
View File

@ -0,0 +1,48 @@
/**
External API surface for HackMyResume.
@license MIT. See LICENSE.md for details.
@module hackmycore/index
*/
/** API facade for HackMyResume. */
(function() {
module.exports = {
verbs: {
build: require('./verbs/build'),
analyze: require('./verbs/analyze'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
"new": require('./verbs/create'),
peek: require('./verbs/peek')
},
alias: {
generate: require('./verbs/build'),
create: require('./verbs/create')
},
options: require('./core/default-options'),
formats: require('./core/default-formats'),
Sheet: require('./core/fresh-resume'),
FRESHResume: require('./core/fresh-resume'),
JRSResume: require('./core/jrs-resume'),
FRESHTheme: require('./core/fresh-theme'),
JRSTheme: require('./core/jrs-theme'),
ResumeFactory: require('./core/resume-factory'),
FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./generators/html-generator'),
TextGenerator: require('./generators/text-generator'),
HtmlPdfCliGenerator: require('./generators/html-pdf-cli-generator'),
WordGenerator: require('./generators/word-generator'),
MarkdownGenerator: require('./generators/markdown-generator'),
JsonGenerator: require('./generators/json-generator'),
YamlGenerator: require('./generators/yaml-generator'),
JsonYamlGenerator: require('./generators/json-yaml-generator'),
LaTeXGenerator: require('./generators/latex-generator'),
HtmlPngGenerator: require('./generators/html-png-generator')
};
}).call(this);
//# sourceMappingURL=index.js.map

140
dist/inspectors/gap-inspector.js vendored Normal file
View File

@ -0,0 +1,140 @@
/**
Employment gap analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module inspectors/gap-inspector
*/
(function() {
var FluentDate, LO, _, gapInspector, moment;
_ = require('underscore');
FluentDate = require('../core/fluent-date');
moment = require('moment');
LO = require('lodash');
/**
Identify gaps in the candidate's employment history.
*/
gapInspector = module.exports = {
moniker: 'gap-inspector',
/**
Run the Gap Analyzer on a resume.
@method run
@return {Array} An array of object representing gaps in the candidate's
employment history. Each object provides the start, end, and duration of the
gap:
{ <-- gap
start: // A Moment.js date
end: // A Moment.js date
duration: // Gap length
}
*/
run: function(rez) {
var coverage, dur, g, gap_start, hist, new_e, num_gaps, o, ref_count, tdur, total_gap_days;
coverage = {
gaps: [],
overlaps: [],
pct: '0%',
duration: {
total: 0,
work: 0,
gaps: 0
}
};
hist = LO.get(rez, 'employment.history');
if (!hist || !hist.length) {
return coverage;
}
new_e = hist.map(function(job) {
var obj;
obj = _.pick(job, ['start', 'end']);
if (obj && (obj.start || obj.end)) {
obj = _.pairs(obj);
obj[0][1] = FluentDate.fmt(obj[0][1]);
if (obj.length > 1) {
obj[1][1] = FluentDate.fmt(obj[1][1]);
}
}
return obj;
});
new_e = _.filter(_.flatten(new_e, true), function(v) {
return v && v.length && v[0] && v[0].length;
});
if (!new_e || !new_e.length) {
return coverage;
}
new_e = _.sortBy(new_e, function(elem) {
return elem[1].unix();
});
num_gaps = 0;
ref_count = 0;
total_gap_days = 0;
gap_start = null;
new_e.forEach(function(point) {
var inc, lastGap, lastOver;
inc = point[0] === 'start' ? 1 : -1;
ref_count += inc;
if (ref_count === 0) {
return coverage.gaps.push({
start: point[1],
end: null
});
} else if (ref_count === 1 && inc === 1) {
lastGap = _.last(coverage.gaps);
if (lastGap) {
lastGap.end = point[1];
lastGap.duration = lastGap.end.diff(lastGap.start, 'days');
return total_gap_days += lastGap.duration;
}
} else if (ref_count === 2 && inc === 1) {
return coverage.overlaps.push({
start: point[1],
end: null
});
} else if (ref_count === 1 && inc === -1) {
lastOver = _.last(coverage.overlaps);
if (lastOver) {
lastOver.end = point[1];
lastOver.duration = lastOver.end.diff(lastOver.start, 'days');
if (lastOver.duration === 0) {
return coverage.overlaps.pop();
}
}
}
});
if (coverage.overlaps.length) {
o = _.last(coverage.overlaps);
if (o && !o.end) {
o.end = moment();
o.duration = o.end.diff(o.start, 'days');
}
}
if (coverage.gaps.length) {
g = _.last(coverage.gaps);
if (g && !g.end) {
g.end = moment();
g.duration = g.end.diff(g.start, 'days');
}
}
tdur = rez.duration('days');
dur = {
total: tdur,
work: tdur - total_gap_days,
gaps: total_gap_days
};
coverage.pct = dur.total > 0 && dur.work > 0 ? (((dur.total - dur.gaps) / dur.total) * 100).toFixed(1) + '%' : '???';
coverage.duration = dur;
return coverage;
}
};
}).call(this);
//# sourceMappingURL=gap-inspector.js.map

63
dist/inspectors/keyword-inspector.js vendored Normal file
View File

@ -0,0 +1,63 @@
/**
Keyword analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module inspectors/keyword-inspector
*/
(function() {
var FluentDate, _, keywordInspector;
_ = require('underscore');
FluentDate = require('../core/fluent-date');
/**
Analyze the resume's use of keywords.
TODO: BUG: Keyword search regex is inaccurate, especially for one or two
letter keywords like "C" or "CLI".
@class keywordInspector
*/
keywordInspector = module.exports = {
/** A unique name for this inspector. */
moniker: 'keyword-inspector',
/**
Run the Keyword Inspector on a resume.
@method run
@return An collection of statistical keyword data.
*/
run: function(rez) {
var prefix, regex_quote, searchable, suffix;
regex_quote = function(str) {
return (str + '').replace(/[.?*+^$[\]\\(){}|-]/ig, "\\$&");
};
searchable = '';
rez.transformStrings(['imp', 'computed', 'safe'], function(key, val) {
return searchable += ' ' + val;
});
prefix = '(?:' + ['^', '\\s+', '[\\.,]+'].join('|') + ')';
suffix = '(?:' + ['$', '\\s+', '[\\.,]+'].join('|') + ')';
return rez.keywords().map(function(kw) {
var count, myArray, regex, regex_str;
regex_str = prefix + regex_quote(kw) + suffix;
regex = new RegExp(regex_str, 'ig');
myArray = null;
count = 0;
while ((myArray = regex.exec(searchable)) !== null) {
count++;
}
return {
name: kw,
count: count
};
});
}
};
}).call(this);
//# sourceMappingURL=keyword-inspector.js.map

51
dist/inspectors/totals-inspector.js vendored Normal file
View File

@ -0,0 +1,51 @@
/**
Section analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module inspectors/totals-inspector
*/
(function() {
var FluentDate, _, totalsInspector;
_ = require('underscore');
FluentDate = require('../core/fluent-date');
/**
Retrieve sectional overview and summary information.
@class totalsInspector
*/
totalsInspector = module.exports = {
moniker: 'totals-inspector',
/**
Run the Totals Inspector on a resume.
@method run
@return An object containing summary information for each section on the
resume.
*/
run: function(rez) {
var sectionTotals;
sectionTotals = {};
_.each(rez, function(val, key) {
if (_.isArray(val) && !_.isString(val)) {
return sectionTotals[key] = val.length;
} else if (val.history && _.isArray(val.history)) {
return sectionTotals[key] = val.history.length;
} else if (val.sets && _.isArray(val.sets)) {
return sectionTotals[key] = val.sets.length;
}
});
return {
totals: sectionTotals,
numSections: Object.keys(sectionTotals).length
};
}
};
}).call(this);
//# sourceMappingURL=totals-inspector.js.map

102
dist/renderers/handlebars-generator.js vendored Normal file
View File

@ -0,0 +1,102 @@
/**
Definition of the HandlebarsGenerator class.
@license MIT. See LICENSE.md for details.
@module renderers/handlebars-generator
*/
(function() {
var FS, HANDLEBARS, HMSTATUS, HandlebarsGenerator, PATH, READFILES, SLASH, _, parsePath, registerHelpers, registerPartials;
_ = require('underscore');
HANDLEBARS = require('handlebars');
FS = require('fs');
registerHelpers = require('../helpers/handlebars-helpers');
PATH = require('path');
parsePath = require('parse-filepath');
READFILES = require('recursive-readdir-sync');
HMSTATUS = require('../core/status-codes');
SLASH = require('slash');
/**
Perform template-based resume generation using Handlebars.js.
@class HandlebarsGenerator
*/
HandlebarsGenerator = module.exports = {
generateSimple: function(data, tpl) {
var template;
try {
template = HANDLEBARS.compile(tpl, {
strict: false,
assumeObjects: false
});
return template(data);
} catch (_error) {
throw {
fluenterror: HMSTATUS[template ? 'invokeTemplate' : 'compileTemplate'],
inner: _error
};
}
},
generate: function(json, jst, format, curFmt, opts, theme) {
var ctx, encData;
registerPartials(format, theme);
registerHelpers(theme, opts);
encData = json;
if (format === 'html' || format === 'pdf') {
encData = json.markdownify();
}
if (format === 'doc') {
encData = json.xmlify();
}
ctx = {
r: encData,
RAW: json,
filt: opts.filters,
format: format,
opts: opts,
engine: this,
results: curFmt.files,
headFragment: opts.headFragment || ''
};
return this.generateSimple(ctx, jst);
}
};
registerPartials = function(format, theme) {
var partialsFolder;
if (_.contains(['html', 'doc', 'md', 'txt', 'pdf'], format)) {
partialsFolder = PATH.join(parsePath(require.resolve('fresh-themes')).dirname, '/partials/', format === 'pdf' ? 'html' : format);
_.each(READFILES(partialsFolder, function(error) {
return {};
}), function(el) {
var compiledTemplate, name, pathInfo, tplData;
pathInfo = parsePath(el);
name = SLASH(PATH.relative(partialsFolder, el).replace(/\.(?:html|xml|hbs|md|txt)$/i, ''));
tplData = FS.readFileSync(el, 'utf8');
compiledTemplate = HANDLEBARS.compile(tplData);
HANDLEBARS.registerPartial(name, compiledTemplate);
return theme.partialsInitialized = true;
});
}
return _.each(theme.partials, function(el) {
var compiledTemplate, tplData;
tplData = FS.readFileSync(el.path, 'utf8');
compiledTemplate = HANDLEBARS.compile(tplData);
return HANDLEBARS.registerPartial(el.name, compiledTemplate);
});
};
}).call(this);
//# sourceMappingURL=handlebars-generator.js.map

61
dist/renderers/jrs-generator.js vendored Normal file
View File

@ -0,0 +1,61 @@
/**
Definition of the JRSGenerator class.
@license MIT. See LICENSE.md for details.
@module renderers/jrs-generator
*/
(function() {
var FS, HANDLEBARS, JRSGenerator, MD, MDIN, PATH, READFILES, SLASH, _, parsePath, registerHelpers;
_ = require('underscore');
HANDLEBARS = require('handlebars');
FS = require('fs');
registerHelpers = require('../helpers/handlebars-helpers');
PATH = require('path');
parsePath = require('parse-filepath');
READFILES = require('recursive-readdir-sync');
SLASH = require('slash');
MD = require('marked');
/**
Perform template-based resume generation for JSON Resume themes.
@class JRSGenerator
*/
JRSGenerator = module.exports = {
generate: function(json, jst, format, cssInfo, opts, theme) {
var org, rezHtml, turnoff;
turnoff = ['log', 'error', 'dir'];
org = turnoff.map(function(c) {
var ret;
ret = console[c];
console[c] = function() {};
return ret;
});
rezHtml = theme.render(json.harden());
turnoff.forEach(function(c, idx) {
return console[c] = org[idx];
});
return rezHtml = rezHtml.replace(/@@@@~.*?~@@@@/gm, function(val) {
return MDIN(val.replace(/~@@@@/gm, '').replace(/@@@@~/gm, ''));
});
}
};
MDIN = function(txt) {
return MD(txt || '').replace(/^\s*<p>|<\/p>\s*$/gi, '');
};
}).call(this);
//# sourceMappingURL=jrs-generator.js.map

87
dist/renderers/underscore-generator.js vendored Normal file
View File

@ -0,0 +1,87 @@
/**
Definition of the UnderscoreGenerator class.
@license MIT. See LICENSE.md for details.
@module underscore-generator.js
*/
(function() {
var UnderscoreGenerator, _, escapeLaTeX, registerHelpers;
_ = require('underscore');
registerHelpers = require('../helpers/underscore-helpers');
require('../utils/string');
escapeLaTeX = require('escape-latex');
/**
Perform template-based resume generation using Underscore.js.
@class UnderscoreGenerator
*/
UnderscoreGenerator = module.exports = {
generateSimple: function(data, tpl) {
var HMS, t;
try {
t = _.template(tpl);
return t(data);
} catch (_error) {
HMS = require('../core/status-codes');
throw {
fluenterror: HMS[t ? 'invokeTemplate' : 'compileTemplate'],
inner: _error
};
}
},
generate: function(json, jst, format, cssInfo, opts, theme) {
var ctx, delims, r, traverse;
delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template;
if (opts.themeObj && opts.themeObj.delimeters) {
delims = _.mapObject(delims, function(val, key) {
return new RegExp(val, "ig");
});
}
_.templateSettings = delims;
r = null;
switch (format) {
case 'html':
r = json.markdownify();
break;
case 'pdf':
r = json.markdownify();
break;
case 'png':
r = json.markdownify();
break;
case 'latex':
traverse = require('traverse');
r = traverse(json).map(function(x) {
if (this.isLeaf && String.is(this.node)) {
return escapeLaTeX(this.node);
}
return this.node;
});
break;
default:
r = json;
}
ctx = {
r: r,
filt: opts.filters,
XML: require('xml-escape'),
RAW: json,
cssInfo: cssInfo,
headFragment: opts.headFragment || '',
opts: opts
};
registerHelpers(theme, opts, cssInfo, ctx, this);
return this.generateSimple(ctx, jst);
}
};
}).call(this);
//# sourceMappingURL=underscore-generator.js.map

14
dist/utils/file-contains.js vendored Normal file
View File

@ -0,0 +1,14 @@
/**
Definition of the SyntaxErrorEx class.
@module file-contains.js
*/
(function() {
module.exports = function(file, needle) {
return require('fs').readFileSync(file, 'utf-8').indexOf(needle) > -1;
};
}).call(this);
//# sourceMappingURL=file-contains.js.map

63
dist/utils/html-to-wpml.js vendored Normal file
View File

@ -0,0 +1,63 @@
/**
Definition of the Markdown to WordProcessingML conversion routine.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module utils/html-to-wpml
*/
(function() {
var HTML5Tokenizer, _;
_ = require('underscore');
HTML5Tokenizer = require('simple-html-tokenizer');
module.exports = function(html) {
var final, is_bold, is_italic, is_link, link_url, tokens;
tokens = HTML5Tokenizer.tokenize(html);
final = is_bold = is_italic = is_link = link_url = '';
_.each(tokens, function(tok) {
var style;
switch (tok.type) {
case 'StartTag':
switch (tok.tagName) {
case 'p':
return final += '<w:p>';
case 'strong':
return is_bold = true;
case 'em':
return is_italic = true;
case 'a':
is_link = true;
return link_url = tok.attributes.filter(function(attr) {
return attr[0] === 'href';
})[0][1];
}
break;
case 'EndTag':
switch (tok.tagName) {
case 'p':
return final += '</w:p>';
case 'strong':
return is_bold = false;
case 'em':
return is_italic = false;
case 'a':
return is_link = false;
}
break;
case 'Chars':
if ((tok.chars.trim().length)) {
style = is_bold ? '<w:b/>' : '';
style += is_italic ? '<w:i/>' : '';
style += is_link ? '<w:rStyle w:val="Hyperlink"/>' : '';
return final += (is_link ? '<w:hlink w:dest="' + link_url + '">' : '') + '<w:r><w:rPr>' + style + '</w:rPr><w:t>' + tok.chars + '</w:t></w:r>' + (is_link ? '</w:hlink>' : '');
}
}
});
return final;
};
}).call(this);
//# sourceMappingURL=html-to-wpml.js.map

30
dist/utils/md2chalk.js vendored Normal file
View File

@ -0,0 +1,30 @@
/**
Inline Markdown-to-Chalk conversion routines.
@license MIT. See LICENSE.md for details.
@module utils/md2chalk
*/
(function() {
var CHALK, LO, MD;
MD = require('marked');
CHALK = require('chalk');
LO = require('lodash');
module.exports = function(v, style, boldStyle) {
var temp;
boldStyle = boldStyle || 'bold';
temp = v.replace(/\*\*(.*?)\*\*/g, LO.get(CHALK, boldStyle)('$1'));
if (style) {
return LO.get(CHALK, style)(temp);
} else {
return temp;
}
};
}).call(this);
//# sourceMappingURL=md2chalk.js.map

79
dist/utils/rasterize.js vendored Normal file
View File

@ -0,0 +1,79 @@
(function() {
"use strict";
var address, output, page, pageHeight, pageWidth, size, system;
page = require('webpage').create();
system = require('system');
address = output = size = null;
if (system.args.length < 3 || system.args.length > 5) {
console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
console.log(' image (png/jpg output) examples: "1920px" entire page, window width 1920px');
console.log(' "800px*600px" window, clipped to 800x600');
phantom.exit(1);
} else {
address = system.args[1];
output = system.args[2];
page.viewportSize = {
width: 600,
height: 600
};
if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
size = system.args[3].split('*');
page.paperSize = size.length === 2 ? {
width: size[0],
height: size[1],
margin: '0px'
} : {
format: system.args[3],
orientation: 'portrait',
margin: '1cm'
};
} else if (system.args.length > 3 && system.args[3].substr(-2) === "px") {
size = system.args[3].split('*');
if (size.length === 2) {
pageWidth = parseInt(size[0], 10);
pageHeight = parseInt(size[1], 10);
page.viewportSize = {
width: pageWidth,
height: pageHeight
};
page.clipRect = {
top: 0,
left: 0,
width: pageWidth,
height: pageHeight
};
} else {
console.log("size:", system.args[3]);
pageWidth = parseInt(system.args[3], 10);
pageHeight = parseInt(pageWidth * 3 / 4, 10);
console.log("pageHeight:", pageHeight);
page.viewportSize = {
width: pageWidth,
height: pageHeight
};
}
}
if (system.args.length > 4) {
page.zoomFactor = system.args[4];
}
page.open(address, function(status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit(1);
} else {
return window.setTimeout(function() {
page.render(output);
phantom.exit();
}, 200);
}
});
}
}).call(this);
//# sourceMappingURL=rasterize.js.map

34
dist/utils/safe-json-loader.js vendored Normal file
View File

@ -0,0 +1,34 @@
/**
Definition of the SafeJsonLoader class.
@module utils/safe-json-loader
@license MIT. See LICENSE.md for details.
*/
(function() {
var FS, SyntaxErrorEx;
FS = require('fs');
SyntaxErrorEx = require('./syntax-error-ex');
module.exports = function(file) {
var ret, retRaw;
ret = {};
try {
ret.raw = FS.readFileSync(file, 'utf8');
ret.json = JSON.parse(ret.raw);
} catch (_error) {
retRaw = ret.raw && ret.raw.trim();
ret.ex = {
operation: retRaw ? 'parse' : 'read',
inner: SyntaxErrorEx.is(_error) ? new SyntaxErrorEx(_error, retRaw) : _error,
file: file
};
}
return ret;
};
}).call(this);
//# sourceMappingURL=safe-json-loader.js.map

46
dist/utils/safe-spawn.js vendored Normal file
View File

@ -0,0 +1,46 @@
/**
Safe spawn utility for HackMyResume / FluentCV.
@module utils/safe-spawn
@license MIT. See LICENSE.md for details.
*/
/** Safely spawn a process synchronously or asynchronously without throwing an
exception
*/
(function() {
module.exports = function(cmd, args, isSync, callback, param) {
var info, spawn;
try {
spawn = require('child_process')[isSync ? 'spawnSync' : 'spawn'];
info = spawn(cmd, args);
if (!isSync) {
info.on('error', function(err) {
if (typeof callback === "function") {
callback(err, param);
}
});
} else {
if (info.error) {
if (typeof callback === "function") {
callback(info.error, param);
}
return {
cmd: cmd,
inner: info.error
};
}
}
} catch (_error) {
if (typeof callback === "function") {
callback(_error, param);
}
return _error;
}
};
}).call(this);
//# sourceMappingURL=safe-spawn.js.map

64
dist/utils/string-transformer.js vendored Normal file
View File

@ -0,0 +1,64 @@
/**
Object string transformation.
@module utils/string-transformer
@license MIT. See LICENSE.md for details.
*/
(function() {
var _, moment;
_ = require('underscore');
moment = require('moment');
/**
Create a copy of this object in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder).
*/
module.exports = function(ret, filt, transformer) {
var that, transformStringsInObject;
that = this;
transformStringsInObject = function(obj, filters) {
if (!obj) {
return;
}
if (moment.isMoment(obj)) {
return;
}
if (_.isArray(obj)) {
return obj.forEach(function(elem, idx, ar) {
if (typeof elem === 'string' || elem instanceof String) {
return ar[idx] = transformer(null, elem);
} else if (_.isObject(elem)) {
return transformStringsInObject(elem, filters);
}
});
} else if (_.isObject(obj)) {
return Object.keys(obj).forEach(function(k) {
var sub;
if (filters.length && _.contains(filters, k)) {
return;
}
sub = obj[k];
if (typeof sub === 'string' || sub instanceof String) {
return obj[k] = transformer(k, sub);
} else if (_.isObject(sub)) {
return transformStringsInObject(sub, filters);
}
});
}
};
Object.keys(ret).forEach(function(member) {
if (!filt || !filt.length || !_.contains(filt, member)) {
return transformStringsInObject(ret[member], filt || []);
}
});
return ret;
};
}).call(this);
//# sourceMappingURL=string-transformer.js.map

29
dist/utils/string.js vendored Normal file
View File

@ -0,0 +1,29 @@
/**
Definitions of string utility functions.
@module utils/string
*/
/**
Determine if the string is null, empty, or whitespace.
See: http://stackoverflow.com/a/32800728/4942583
@method isNullOrWhitespace
*/
(function() {
String.isNullOrWhitespace = function(input) {
return !input || !input.trim();
};
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
String.is = function(val) {
return typeof val === 'string' || val instanceof String;
};
}).call(this);
//# sourceMappingURL=string.js.map

55
dist/utils/syntax-error-ex.js vendored Normal file
View File

@ -0,0 +1,55 @@
/**
Definition of the SyntaxErrorEx class.
@module utils/syntax-error-ex
@license MIT. See LICENSE.md for details.
*/
/**
Represents a SyntaxError exception with line and column info.
Collect syntax error information from the provided exception object. The
JavaScript `SyntaxError` exception isn't interpreted uniformly across environ-
ments, so we reparse on error to grab the line and column.
See: http://stackoverflow.com/q/13323356
@class SyntaxErrorEx
*/
(function() {
var SyntaxErrorEx;
SyntaxErrorEx = (function() {
function SyntaxErrorEx(ex, rawData) {
var JSONLint, colNum, lineNum, lint, ref;
lineNum = null;
colNum = null;
JSONLint = require('json-lint');
lint = JSONLint(rawData, {
comments: false
});
if (lint.error) {
ref = [lint.line, lint.character], this.line = ref[0], this.col = ref[1];
}
if (!lint.error) {
JSONLint = require('jsonlint');
try {
JSONLint.parse(rawData);
} catch (_error) {
this.line = (/on line (\d+)/.exec(_error))[1];
}
}
}
return SyntaxErrorEx;
})();
SyntaxErrorEx.is = function(ex) {
return ex instanceof SyntaxError;
};
module.exports = SyntaxErrorEx;
}).call(this);
//# sourceMappingURL=syntax-error-ex.js.map

110
dist/verbs/analyze.js vendored Normal file
View File

@ -0,0 +1,110 @@
/**
Implementation of the 'analyze' verb for HackMyResume.
@module verbs/analyze
@license MIT. See LICENSE.md for details.
*/
(function() {
var AnalyzeVerb, HMEVENT, HMSTATUS, MKDIRP, PATH, ResumeFactory, Verb, _, _analyze, _analyzeOne, _loadInspectors, chalk,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
MKDIRP = require('mkdirp');
PATH = require('path');
HMEVENT = require('../core/event-codes');
HMSTATUS = require('../core/status-codes');
_ = require('underscore');
ResumeFactory = require('../core/resume-factory');
Verb = require('../verbs/verb');
chalk = require('chalk');
/** An invokable resume analysis command. */
module.exports = AnalyzeVerb = (function(superClass) {
extend(AnalyzeVerb, superClass);
function AnalyzeVerb() {
AnalyzeVerb.__super__.constructor.call(this, 'analyze', _analyze);
}
return AnalyzeVerb;
})(Verb);
/** Private workhorse for the 'analyze' command. */
_analyze = function(sources, dst, opts) {
var nlzrs, results;
if (!sources || !sources.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
nlzrs = _loadInspectors();
results = _.map(sources, function(src) {
var r;
r = ResumeFactory.loadOne(src, {
format: 'FRESH',
objectify: true
}, this);
if (opts.assert && this.hasError()) {
return {};
}
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
return r;
} else {
return _analyzeOne.call(this, r, nlzrs, opts);
}
}, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Analyze a single resume. */
_analyzeOne = function(resumeObject, nlzrs, opts) {
var info, rez, safeFormat;
rez = resumeObject.rez;
safeFormat = rez.meta && rez.meta.format && rez.meta.format.startsWith('FRESH') ? 'FRESH' : 'JRS';
this.stat(HMEVENT.beforeAnalyze, {
fmt: safeFormat,
file: resumeObject.file
});
info = _.mapObject(nlzrs, function(val, key) {
return val.run(rez);
});
this.stat(HMEVENT.afterAnalyze, {
info: info
});
return info;
};
_loadInspectors = function() {
return {
totals: require('../inspectors/totals-inspector'),
coverage: require('../inspectors/gap-inspector'),
keywords: require('../inspectors/keyword-inspector')
};
};
}).call(this);
//# sourceMappingURL=analyze.js.map

450
dist/verbs/build.js vendored Normal file
View File

@ -0,0 +1,450 @@
/**
Implementation of the 'build' verb for HackMyResume.
@module verbs/build
@license MIT. See LICENSE.md for details.
*/
(function() {
var BuildVerb, FRESHTheme, FS, HMEVENT, HMSTATUS, JRSTheme, MD, MKDIRP, PATH, RConverter, RTYPES, ResumeFactory, Verb, _, _addFreebieFormats, _build, _err, _expand, _fmts, _loadTheme, _log, _opts, _prep, _rezObj, _single, _verifyOutputs, _verifyTheme, addFreebieFormats, build, expand, extend, loadTheme, parsePath, prep, single, verifyOutputs, verifyTheme,
extend1 = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
_ = require('underscore');
PATH = require('path');
FS = require('fs');
MD = require('marked');
MKDIRP = require('mkdirp');
extend = require('extend');
parsePath = require('parse-filepath');
RConverter = require('fresh-jrs-converter');
HMSTATUS = require('../core/status-codes');
HMEVENT = require('../core/event-codes');
RTYPES = {
FRESH: require('../core/fresh-resume'),
JRS: require('../core/jrs-resume')
};
_opts = require('../core/default-options');
FRESHTheme = require('../core/fresh-theme');
JRSTheme = require('../core/jrs-theme');
ResumeFactory = require('../core/resume-factory');
_fmts = require('../core/default-formats');
Verb = require('../verbs/verb');
_err = null;
_log = null;
_rezObj = null;
build = null;
prep = null;
single = null;
verifyOutputs = null;
addFreebieFormats = null;
expand = null;
verifyTheme = null;
loadTheme = null;
/** An invokable resume generation command. */
module.exports = BuildVerb = (function(superClass) {
extend1(BuildVerb, superClass);
/** Create a new build verb. */
function BuildVerb() {
BuildVerb.__super__.constructor.call(this, 'build', _build);
}
return BuildVerb;
})(Verb);
/**
Given a source resume in FRESH or JRS format, 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 opts Generation options.
*/
_build = function(src, dst, opts) {
var inv, isFRESH, mixed, newEx, orgFormat, problemSheets, results, rez, sheetObjects, sheets, tFolder, targets, theme, toFormat;
if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
_prep.call(this, src, dst, opts);
sheetObjects = ResumeFactory.load(src, {
format: null,
objectify: false,
quit: true,
inner: {
sort: _opts.sort
}
}, this);
problemSheets = _.filter(sheetObjects, function(so) {
return so.fluenterror;
});
if (problemSheets && problemSheets.length) {
problemSheets[0].quit = true;
this.err(problemSheets[0].fluenterror, problemSheets[0]);
return null;
}
sheets = sheetObjects.map(function(r) {
return r.json;
});
theme = null;
this.stat(HMEVENT.beforeTheme, {
theme: _opts.theme
});
try {
tFolder = _verifyTheme.call(this, _opts.theme);
if (tFolder.fluenterror) {
tFolder.quit = true;
this.err(tFolder.fluenterror, tFolder);
return;
}
theme = _opts.themeObj = _loadTheme(tFolder);
_addFreebieFormats(theme);
} catch (_error) {
newEx = {
fluenterror: HMSTATUS.themeLoad,
inner: _error,
attempted: _opts.theme,
quit: true
};
this.err(HMSTATUS.themeLoad, newEx);
return null;
}
this.stat(HMEVENT.afterTheme, {
theme: theme
});
inv = _verifyOutputs.call(this, dst, theme);
if (inv && inv.length) {
this.err(HMSTATUS.invalidFormat, {
data: inv,
theme: theme,
quit: true
});
return null;
}
rez = null;
if (sheets.length > 1) {
isFRESH = !sheets[0].basics;
mixed = _.any(sheets, function(s) {
if (isFRESH) {
return s.basics;
} else {
return !s.basics;
}
});
this.stat(HMEVENT.beforeMerge, {
f: _.clone(sheetObjects),
mixed: mixed
});
if (mixed) {
this.err(HMSTATUS.mixedMerge);
}
rez = _.reduceRight(sheets, function(a, b, idx) {
return extend(true, b, a);
});
this.stat(HMEVENT.afterMerge, {
r: rez
});
} else {
rez = sheets[0];
}
orgFormat = rez.basics ? 'JRS' : 'FRESH';
toFormat = theme.render ? 'JRS' : 'FRESH';
if (toFormat !== orgFormat) {
this.stat(HMEVENT.beforeInlineConvert);
rez = RConverter['to' + toFormat](rez);
this.stat(HMEVENT.afterInlineConvert, {
file: sheetObjects[0].file,
fmt: toFormat
});
}
this.stat(HMEVENT.applyTheme, {
r: rez,
theme: theme
});
_rezObj = new RTYPES[toFormat]().parseJSON(rez);
targets = _expand(dst, theme);
_.each(targets, function(t) {
var ref;
if (this.hasError() && opts.assert) {
return {};
}
t.final = _single.call(this, t, theme, targets);
if ((ref = t.final) != null ? ref.fluenterror : void 0) {
t.final.quit = opts.assert;
this.err(t.final.fluenterror, t.final);
}
}, this);
results = {
sheet: _rezObj,
targets: targets,
processed: targets
};
if (this.hasError() && !opts.assert) {
this.reject(results);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/**
Prepare for a BUILD run.
*/
_prep = function(src, dst, opts) {
var that;
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
_opts.prettify = opts.prettify === true;
_opts.css = opts.css;
_opts.pdf = opts.pdf;
_opts.wrap = opts.wrap || 60;
_opts.stitles = opts.sectionTitles;
_opts.tips = opts.tips;
_opts.errHandler = opts.errHandler;
_opts.noTips = opts.noTips;
_opts.debug = opts.debug;
_opts.sort = opts.sort;
that = this;
_opts.onTransform = function(info) {
that.stat(HMEVENT.afterTransform, info);
};
_opts.beforeWrite = function(info) {
that.stat(HMEVENT.beforeWrite, info);
};
_opts.afterWrite = function(info) {
that.stat(HMEVENT.afterWrite, info);
};
(src.length > 1 && (!dst || !dst.length)) && dst.push(src.pop());
};
/**
Generate a single target resume such as "out/rez.html" or "out/rez.doc".
TODO: Refactor.
@param targInfo Information for the target resume.
@param theme A FRESHTheme or JRSTheme object.
*/
_single = function(targInfo, theme, finished) {
var e, ex, f, fName, fType, outFolder, ret, theFormat;
ret = null;
ex = null;
f = targInfo.file;
try {
if (!targInfo.fmt) {
return {};
}
fType = targInfo.fmt.outFormat;
fName = PATH.basename(f, '.' + fType);
theFormat = null;
this.stat(HMEVENT.beforeGenerate, {
fmt: targInfo.fmt.outFormat,
file: PATH.relative(process.cwd(), f)
});
_opts.targets = finished;
if (targInfo.fmt.files && targInfo.fmt.files.length) {
theFormat = _fmts.filter(function(fmt) {
return fmt.name === targInfo.fmt.outFormat;
})[0];
MKDIRP.sync(PATH.dirname(f));
ret = theFormat.gen.generate(_rezObj, f, _opts);
} else {
theFormat = _fmts.filter(function(fmt) {
return fmt.name === targInfo.fmt.outFormat;
})[0];
outFolder = PATH.dirname(f);
MKDIRP.sync(outFolder);
ret = theFormat.gen.generate(_rezObj, f, _opts);
}
} catch (_error) {
e = _error;
ex = e;
}
this.stat(HMEVENT.afterGenerate, {
fmt: targInfo.fmt.outFormat,
file: PATH.relative(process.cwd(), f),
error: ex
});
if (ex) {
if (ex.fluenterror) {
ret = ex;
} else {
ret = {
fluenterror: HMSTATUS.generateError,
inner: ex
};
}
}
return ret;
};
/** Ensure that user-specified outputs/targets are valid. */
_verifyOutputs = function(targets, theme) {
this.stat(HMEVENT.verifyOutputs, {
targets: targets,
theme: theme
});
return _.reject(targets.map(function(t) {
var pathInfo;
pathInfo = parsePath(t);
return {
format: pathInfo.extname.substr(1)
};
}), function(t) {
return t.format === 'all' || theme.hasFormat(t.format);
});
};
/**
Reinforce the chosen theme with "freebie" formats provided by HackMyResume.
A "freebie" format is an output format such as JSON, YML, or PNG that can be
generated directly from the resume model or from one of the theme's declared
output formats. For example, the PNG format can be generated for any theme
that declares an HTML format; the theme doesn't have to provide an explicit
PNG template.
@param theTheme A FRESHTheme or JRSTheme object.
*/
_addFreebieFormats = function(theTheme) {
theTheme.formats.json = theTheme.formats.json || {
freebie: true,
title: 'json',
outFormat: 'json',
pre: 'json',
ext: 'json',
path: null,
data: null
};
theTheme.formats.yml = theTheme.formats.yml || {
freebie: true,
title: 'yaml',
outFormat: 'yml',
pre: 'yml',
ext: 'yml',
path: null,
data: null
};
if (theTheme.formats.html && !theTheme.formats.png) {
theTheme.formats.png = {
freebie: true,
title: 'png',
outFormat: 'png',
ext: 'yml',
path: null,
data: null
};
}
};
/**
Expand output files. For example, "foo.all" should be expanded to
["foo.html", "foo.doc", "foo.pdf", "etc"].
@param dst An array of output files as specified by the user.
@param theTheme A FRESHTheme or JRSTheme object.
*/
_expand = function(dst, theTheme) {
var destColl, targets;
destColl = (dst && dst.length && dst) || [PATH.normalize('out/resume.all')];
targets = [];
destColl.forEach(function(t) {
var fmat, pa, to;
to = PATH.resolve(t);
pa = parsePath(to);
fmat = pa.extname || '.all';
return targets.push.apply(targets, fmat === '.all' ? Object.keys(theTheme.formats).map(function(k) {
var z;
z = theTheme.formats[k];
return {
file: to.replace(/all$/g, z.outFormat),
fmt: z
};
}) : [
{
file: to,
fmt: theTheme.getFormat(fmat.slice(1))
}
]);
});
return targets;
};
/**
Verify the specified theme name/path.
*/
_verifyTheme = function(themeNameOrPath) {
var exists, tFolder;
tFolder = PATH.join(parsePath(require.resolve('fresh-themes')).dirname, '/themes/', themeNameOrPath);
exists = require('path-exists').sync;
if (!exists(tFolder)) {
tFolder = PATH.resolve(themeNameOrPath);
if (!exists(tFolder)) {
return {
fluenterror: HMSTATUS.themeNotFound,
data: _opts.theme
};
}
}
return tFolder;
};
/**
Load the specified theme, which could be either a FRESH theme or a JSON Resume
theme.
*/
_loadTheme = function(tFolder) {
var theTheme;
theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ? new JRSTheme().open(tFolder) : new FRESHTheme().open(tFolder);
_opts.themeObj = theTheme;
return theTheme;
};
}).call(this);
//# sourceMappingURL=build.js.map

115
dist/verbs/convert.js vendored Normal file
View File

@ -0,0 +1,115 @@
/**
Implementation of the 'convert' verb for HackMyResume.
@module verbs/convert
@license MIT. See LICENSE.md for details.
*/
(function() {
var ConvertVerb, HMEVENT, HMSTATUS, ResumeFactory, Verb, _, _convert, _convertOne, chalk,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
ResumeFactory = require('../core/resume-factory');
chalk = require('chalk');
Verb = require('../verbs/verb');
HMSTATUS = require('../core/status-codes');
_ = require('underscore');
HMEVENT = require('../core/event-codes');
module.exports = ConvertVerb = (function(superClass) {
extend(ConvertVerb, superClass);
function ConvertVerb() {
ConvertVerb.__super__.constructor.call(this, 'convert', _convert);
}
return ConvertVerb;
})(Verb);
/** Private workhorse method. Convert 0..N resumes between FRESH and JRS
formats.
*/
_convert = function(srcs, dst, opts) {
var results;
if (!srcs || !srcs.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
if (!dst || !dst.length) {
if (srcs.length === 1) {
this.err(HMSTATUS.inputOutputParity, {
quit: true
});
} else if (srcs.length === 2) {
dst = dst || [];
dst.push(srcs.pop());
} else {
this.err(HMSTATUS.inputOutputParity, {
quit: true
});
}
}
if (srcs && dst && srcs.length && dst.length && srcs.length !== dst.length) {
this.err(HMSTATUS.inputOutputParity, {
quit: true
});
}
results = _.map(srcs, function(src, idx) {
var r;
if (opts.assert && this.hasError()) {
return {};
}
r = _convertOne.call(this, src, dst, idx);
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
}
return r;
}, this);
if (this.hasError() && !opts.assert) {
this.reject(results);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Private workhorse method. Convert a single resume. */
_convertOne = function(src, dst, idx) {
var rinfo, s, srcFmt, targetFormat;
rinfo = ResumeFactory.loadOne(src, {
format: null,
objectify: true
});
if (rinfo.fluenterror) {
return rinfo;
}
s = rinfo.rez;
srcFmt = ((s.basics && s.basics.imp) || s.imp).orgFormat === 'JRS' ? 'JRS' : 'FRESH';
targetFormat = srcFmt === 'JRS' ? 'FRESH' : 'JRS';
this.stat(HMEVENT.beforeConvert, {
srcFile: rinfo.file,
srcFmt: srcFmt,
dstFile: dst[idx],
dstFmt: targetFormat
});
s.saveAs(dst[idx], targetFormat);
return s;
};
}).call(this);
//# sourceMappingURL=convert.js.map

103
dist/verbs/create.js vendored Normal file
View File

@ -0,0 +1,103 @@
/**
Implementation of the 'create' verb for HackMyResume.
@module verbs/create
@license MIT. See LICENSE.md for details.
*/
(function() {
var CreateVerb, HMEVENT, HMSTATUS, MKDIRP, PATH, Verb, _, _create, _createOne, chalk,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
MKDIRP = require('mkdirp');
PATH = require('path');
chalk = require('chalk');
Verb = require('../verbs/verb');
_ = require('underscore');
HMSTATUS = require('../core/status-codes');
HMEVENT = require('../core/event-codes');
module.exports = CreateVerb = (function(superClass) {
extend(CreateVerb, superClass);
function CreateVerb() {
CreateVerb.__super__.constructor.call(this, 'new', _create);
}
return CreateVerb;
})(Verb);
/** Create a new empty resume in either FRESH or JRS format. */
_create = function(src, dst, opts) {
var results;
if (!src || !src.length) {
this.err(HMSTATUS.createNameMissing, {
quit: true
});
return null;
}
results = _.map(src, function(t) {
var r;
if (opts.assert && this.hasError()) {
return {};
}
r = _createOne.call(this, t, opts);
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
}
return r;
}, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Create a single new resume */
_createOne = function(t, opts) {
var RezClass, newRez, ret, safeFmt;
try {
ret = null;
safeFmt = opts.format.toUpperCase();
this.stat(HMEVENT.beforeCreate, {
fmt: safeFmt,
file: t
});
MKDIRP.sync(PATH.dirname(t));
RezClass = require('../core/' + safeFmt.toLowerCase() + '-resume');
newRez = RezClass["default"]();
newRez.save(t);
ret = newRez;
} catch (_error) {
ret = {
fluenterror: HMSTATUS.createError,
inner: _error
};
} finally {
this.stat(HMEVENT.afterCreate, {
fmt: safeFmt,
file: t,
isError: ret.fluenterror
});
return ret;
}
};
}).call(this);
//# sourceMappingURL=create.js.map

106
dist/verbs/peek.js vendored Normal file
View File

@ -0,0 +1,106 @@
/**
Implementation of the 'peek' verb for HackMyResume.
@module verbs/peek
@license MIT. See LICENSE.md for details.
*/
(function() {
var HMEVENT, HMSTATUS, PeekVerb, Verb, _, __, _peek, _peekOne, safeLoadJSON,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
Verb = require('../verbs/verb');
_ = require('underscore');
__ = require('lodash');
safeLoadJSON = require('../utils/safe-json-loader');
HMSTATUS = require('../core/status-codes');
HMEVENT = require('../core/event-codes');
module.exports = PeekVerb = (function(superClass) {
extend(PeekVerb, superClass);
function PeekVerb() {
PeekVerb.__super__.constructor.call(this, 'peek', _peek);
}
return PeekVerb;
})(Verb);
/** Peek at a resume, resume section, or resume field. */
_peek = function(src, dst, opts) {
var objPath, results;
if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
objPath = (dst && dst[0]) || '';
results = _.map(src, function(t) {
var tgt;
if (opts.assert && this.hasError()) {
return {};
}
tgt = _peekOne.call(this, t, objPath);
if (tgt.error) {
this.setError(tgt.error.fluenterror, tgt.error);
}
return tgt;
}, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Peek at a single resume, resume section, or resume field. */
_peekOne = function(t, objPath) {
var errCode, obj, pkgError, tgt;
this.stat(HMEVENT.beforePeek, {
file: t,
target: objPath
});
obj = safeLoadJSON(t);
tgt = null;
if (!obj.ex) {
tgt = objPath ? __.get(obj.json, objPath) : obj.json;
}
pkgError = null;
if (obj.ex) {
errCode = obj.ex.operation === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError;
if (errCode === HMSTATUS.readError) {
obj.ex.quiet = true;
}
pkgError = {
fluenterror: errCode,
inner: obj.ex
};
}
this.stat(HMEVENT.afterPeek, {
file: t,
requested: objPath,
target: obj.ex ? void 0 : tgt,
error: pkgError
});
return {
val: obj.ex ? void 0 : tgt,
error: pkgError
};
};
}).call(this);
//# sourceMappingURL=peek.js.map

139
dist/verbs/validate.js vendored Normal file
View File

@ -0,0 +1,139 @@
/**
Implementation of the 'validate' verb for HackMyResume.
@module verbs/validate
@license MIT. See LICENSE.md for details.
*/
(function() {
var FS, HMEVENT, HMSTATUS, ResumeFactory, SyntaxErrorEx, ValidateVerb, Verb, _, _validate, _validateOne, chalk, safeLoadJSON,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
FS = require('fs');
ResumeFactory = require('../core/resume-factory');
SyntaxErrorEx = require('../utils/syntax-error-ex');
chalk = require('chalk');
Verb = require('../verbs/verb');
HMSTATUS = require('../core/status-codes');
HMEVENT = require('../core/event-codes');
_ = require('underscore');
safeLoadJSON = require('../utils/safe-json-loader');
/** An invokable resume validation command. */
module.exports = ValidateVerb = (function(superClass) {
extend(ValidateVerb, superClass);
function ValidateVerb() {
ValidateVerb.__super__.constructor.call(this, 'validate', _validate);
}
return ValidateVerb;
})(Verb);
_validate = function(sources, unused, opts) {
var results, schemas, validator;
if (!sources || !sources.length) {
this.err(HMSTATUS.resumeNotFoundAlt, {
quit: true
});
return null;
}
validator = require('is-my-json-valid');
schemas = {
fresh: require('fresca'),
jars: require('../core/resume.json')
};
results = _.map(sources, function(t) {
var r;
r = _validateOne.call(this, t, validator, schemas, opts);
if (r.error) {
this.err(r.error.fluenterror, r.error);
}
return r;
}, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/**
Validate a single resume.
@returns {
file: <fileName>,
isValid: <validFlag>,
status: <validationStatus>,
violations: <validationErrors>,
schema: <schemaType>,
error: <errorObject>
}
*/
_validateOne = function(t, validator, schemas, opts) {
var errCode, obj, ret, validate;
ret = {
file: t,
isValid: false,
status: 'unknown',
schema: '-----'
};
try {
obj = safeLoadJSON(t);
if (!obj.ex) {
if (obj.json.basics) {
ret.schema = 'jars';
} else {
ret.schema = 'fresh';
}
validate = validator(schemas[ret.schema], {
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
ret.isValid = validate(obj.json);
ret.status = ret.isValid ? 'valid' : 'invalid';
if (!ret.isValid) {
ret.violations = validate.errors;
}
} else {
if (obj.ex.operation === 'parse') {
errCode = HMSTATUS.parseError;
ret.status = 'broken';
} else {
errCode = HMSTATUS.readError;
ret.status = 'missing';
}
ret.error = {
fluenterror: errCode,
inner: obj.ex.inner,
quiet: errCode === HMSTATUS.readError
};
}
} catch (_error) {
ret.error = {
fluenterror: HMSTATUS.validateError,
inner: _error
};
}
this.stat(HMEVENT.afterValidate, ret);
return ret;
};
}).call(this);
//# sourceMappingURL=validate.js.map

118
dist/verbs/verb.js vendored Normal file
View File

@ -0,0 +1,118 @@
/**
Definition of the Verb class.
@module verbs/verb
@license MIT. See LICENSE.md for details.
*/
(function() {
var EVENTS, HMEVENT, Promise, Verb;
EVENTS = require('events');
HMEVENT = require('../core/event-codes');
Promise = require('pinkie-promise');
/**
An abstract invokable verb.
Provides base class functionality for verbs. Provide common services such as
error handling, event management, and promise support.
@class Verb
*/
module.exports = Verb = (function() {
/** Constructor. Automatically called at creation. */
function Verb(moniker, workhorse) {
this.moniker = moniker;
this.workhorse = workhorse;
this.emitter = new EVENTS.EventEmitter();
return;
}
/** Invoke the command. */
Verb.prototype.invoke = function() {
var argsArray, that;
this.stat(HMEVENT.begin, {
cmd: this.moniker
});
argsArray = Array.prototype.slice.call(arguments);
that = this;
return this.promise = new Promise(function(res, rej) {
that.resolve = res;
that.reject = rej;
that.workhorse.apply(that, argsArray);
});
};
/** Forward subscriptions to the event emitter. */
Verb.prototype.on = function() {
return this.emitter.on.apply(this.emitter, arguments);
};
/** Fire an arbitrary event, scoped to "hmr:". */
Verb.prototype.fire = function(evtName, payload) {
payload = payload || {};
payload.cmd = this.moniker;
this.emitter.emit('hmr:' + evtName, payload);
return true;
};
/** Handle an error condition. */
Verb.prototype.err = function(errorCode, payload, hot) {
payload = payload || {};
payload.sub = payload.fluenterror = errorCode;
payload["throw"] = hot;
this.setError(errorCode, payload);
if (payload.quit) {
this.reject(errorCode);
}
this.fire('error', payload);
if (hot) {
throw payload;
}
return true;
};
/** Fire the 'hmr:status' error event. */
Verb.prototype.stat = function(subEvent, payload) {
payload = payload || {};
payload.sub = subEvent;
this.fire('status', payload);
return true;
};
/** Has an error occurred during this verb invocation? */
Verb.prototype.hasError = function() {
return this.errorCode || this.errorObj;
};
/** Associate error info with the invocation. */
Verb.prototype.setError = function(code, obj) {
this.errorCode = code;
this.errorObj = obj;
};
return Verb;
})();
}).call(this);
//# sourceMappingURL=verb.js.map

View File

@ -1,13 +1,13 @@
{
"name": "hackmyresume",
"version": "1.7.0",
"version": "1.8.0",
"description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.",
"repository": {
"type": "git",
"url": "https://github.com/hacksalot/HackMyResume.git"
},
"scripts": {
"test": "grunt clean && mocha",
"test": "grunt clean:test && mocha",
"grunt": "grunt"
},
"keywords": [
@ -40,34 +40,58 @@
"license": "MIT",
"preferGlobal": "true",
"bugs": {
"url": "https://github.com/hacksalot/HackMyResume/issues"
"url": "https://github.com/JuanCanham/HackMyResume/issues"
},
"bin": {
"hackmyresume": "src/index.js"
"hackmyresume": "dist/cli/index.js"
},
"homepage": "https://github.com/hacksalot/HackMyResume",
"main": "dist/index.js",
"homepage": "https://github.com/JuanCanham/HackMyResume",
"dependencies": {
"chalk": "^1.1.1",
"commander": "^2.9.0",
"copy": "^0.1.3",
"escape-latex": "^0.1.2",
"extend": "^3.0.0",
"fresca": "~0.6.0",
"hackmycore": "^0.2.0",
"fresh-jrs-converter": "^0.2.2",
"fresh-resume-starter": "^0.2.2",
"fresh-themes": "git+https://git.juancanham.com/JuanCanham/fresh-themes.git#feature/certifications",
"fs-extra": "^0.26.4",
"handlebars": "^4.0.5",
"html": "0.0.10",
"is-my-json-valid": "^2.12.4",
"json-lint": "^0.1.0",
"jsonlint": "^1.6.2",
"lodash": "^3.10.1",
"marked": "^0.3.5",
"mkdirp": "^0.5.1",
"moment": "^2.11.1",
"parse-filepath": "^0.6.3",
"path-exists": "^2.1.0",
"pinkie-promise": "^2.0.0",
"printf": "^0.2.3",
"recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.2.1",
"slash": "^1.0.0",
"string-padding": "^1.0.2",
"string.prototype.endswith": "^0.2.0",
"string.prototype.startswith": "^0.2.0",
"traverse": "^0.6.6",
"underscore": "^1.8.3",
"word-wrap": "^1.1.0",
"xml-escape": "^1.0.0",
"yamljs": "^0.2.4"
},
"devDependencies": {
"chai": "*",
"fresh-test-resumes": "^0.6.0",
"dir-compare": "0.0.2",
"fresh-test-resumes": "^0.7.0",
"grunt": "*",
"grunt-cli": "^0.1.13",
"grunt-contrib-clean": "^0.7.0",
"grunt-contrib-coffee": "^0.13.0",
"grunt-contrib-copy": "^0.8.2",
"grunt-contrib-jshint": "^0.11.3",
"grunt-contrib-yuidoc": "^0.10.0",
"grunt-jsdoc": "^1.1.0",
@ -77,6 +101,7 @@
"jsonresume-theme-modern": "0.0.18",
"jsonresume-theme-sceptile": "^1.0.5",
"mocha": "*",
"resample": "fluentdesk/resample"
"resample": "fluentdesk/resample",
"stripcolorcodes": "^0.1.0"
}
}

232
src/cli/error.coffee Normal file
View File

@ -0,0 +1,232 @@
###*
Error-handling routines for HackMyResume.
@module cli/error
@license MIT. See LICENSE.md for details.
###
HMSTATUS = require '../core/status-codes'
PKG = require '../../package.json'
FS = require 'fs'
FCMD = require '../index'
PATH = require 'path'
WRAP = require 'word-wrap'
M2C = require '../utils/md2chalk'
chalk = require 'chalk'
extend = require 'extend'
YAML = require 'yamljs'
printf = require 'printf'
SyntaxErrorEx = require '../utils/syntax-error-ex'
require 'string.prototype.startswith'
###* Error handler for HackMyResume. All errors are handled here.
@class ErrorHandler ###
ErrorHandler = module.exports =
init: ( debug, assert, silent ) ->
@debug = debug
@assert = assert
@silent = silent
@msgs = require('./msg').errors
@
err: ( ex, shouldExit ) ->
# Short-circuit logging output if --silent is on
o = if @silent then () -> else _defaultLog
# Special case; can probably be removed.
throw ex if ex.pass
# Load error messages
@msgs = @msgs || require('./msg').errors
# Handle packaged HMR exceptions
if ex.fluenterror
# Output the error message
objError = assembleError.call @, ex
o( @[ 'format_' + objError.etype ]( objError.msg ))
# Output the stack (sometimes)
if objError.withStack
stack = ex.stack || (ex.inner && ex.inner.stack);
stack && o( chalk.gray( stack ) );
# Quit if necessary
if shouldExit
if @debug
o chalk.cyan('Exiting with error code ' + ex.fluenterror.toString())
if @assert
ex.pass = true
throw ex
process.exit ex.fluenterror
# Handle raw exceptions
else
o ex
stackTrace = ex.stack || (ex.inner && ex.inner.stack)
if stackTrace && this.debug
o M2C(ex.stack || ex.inner.stack, 'gray')
format_error: ( msg ) ->
msg = msg || ''
chalk.red.bold( if msg.toUpperCase().startsWith('ERROR:') then msg else 'Error: ' + msg )
format_warning: ( brief, msg ) ->
chalk.yellow(brief) + chalk.yellow(msg || '')
format_custom: ( msg ) -> msg
_defaultLog = () -> console.log.apply console.log, arguments
assembleError = ( ex ) ->
msg = ''
withStack = false
quit = false
etype = 'warning'
withStack = true if @debug
switch ex.fluenterror
when HMSTATUS.themeNotFound
msg = printf( M2C( this.msgs.themeNotFound.msg, 'yellow' ), ex.data)
when HMSTATUS.copyCSS
msg = M2C( this.msgs.copyCSS.msg, 'red' )
quit = false
when HMSTATUS.resumeNotFound
msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' );
when HMSTATUS.missingCommand
msg = M2C( this.msgs.missingCommand.msg + " (", 'yellow');
msg += Object.keys( FCMD.verbs ).map( (v, idx, ar) ->
return ( if idx == ar.length - 1 then chalk.yellow('or ') else '') +
chalk.yellow.bold(v.toUpperCase());
).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += chalk.gray(FS.readFileSync(
PATH.resolve(__dirname, '../cli/use.txt'), 'utf8' ))
when HMSTATUS.invalidCommand
msg = printf( M2C( this.msgs.invalidCommand.msg, 'yellow'), ex.attempted )
when HMSTATUS.resumeNotFoundAlt
msg = M2C( this.msgs.resumeNotFoundAlt.msg, 'yellow' )
when HMSTATUS.inputOutputParity
msg = M2C( this.msgs.inputOutputParity.msg )
when HMSTATUS.createNameMissing
msg = M2C( this.msgs.createNameMissing.msg )
when HMSTATUS.pdfGeneration
msg = M2C( this.msgs.pdfGeneration.msg, 'bold' )
msg += chalk.red('\n' + ex.inner) if ex.inner
quit = false
etype = 'error'
when HMSTATUS.invalid
msg = M2C( this.msgs.invalid.msg, 'red' )
etype = 'error'
when HMSTATUS.generateError
msg = (ex.inner && ex.inner.toString()) || ex
quit = false
etype = 'error'
when HMSTATUS.fileSaveError
msg = printf( M2C( this.msgs.fileSaveError.msg ), (ex.inner || ex).toString() )
etype = 'error'
quit = false
when HMSTATUS.invalidFormat
ex.data.forEach( (d) ->
msg += printf( M2C( this.msgs.invalidFormat.msg, 'bold' ),
ex.theme.name.toUpperCase(), d.format.toUpperCase())
, @);
when HMSTATUS.missingParam
msg = printf(M2C( this.msgs.missingParam.msg ), ex.expected, ex.helper)
when HMSTATUS.invalidHelperUse
msg = printf( M2C( this.msgs.invalidHelperUse.msg ), ex.helper )
if ex.error
msg += '\n--> ' + assembleError.call( this, extend( true, {}, ex, {fluenterror: ex.error} )).msg;
#msg += printf( '\n--> ' + M2C( this.msgs.invalidParamCount.msg ), ex.expected );
quit = false
etype = 'warning'
when HMSTATUS.notOnPath
msg = printf( M2C(this.msgs.notOnPath.msg, 'bold'), ex.engine)
quit = false
etype = 'error'
when HMSTATUS.readError
if !ex.quiet
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file))
msg = ex.inner.toString()
etype = 'error'
when HMSTATUS.mixedMerge
msg = M2C this.msgs.mixedMerge.msg
quit = false
when HMSTATUS.invokeTemplate
msg = M2C this.msgs.invokeTemplate.msg, 'red'
msg += M2C( '\n' + WRAP(ex.inner.toString(), { width: 60, indent: ' ' }), 'gray' );
etype = 'custom'
when HMSTATUS.compileTemplate
etype = 'error'
when HMSTATUS.themeLoad
msg = M2C( printf( this.msgs.themeLoad.msg, ex.attempted.toUpperCase() ), 'red');
if ex.inner && ex.inner.fluenterror
msg += M2C('\nError: ', 'red') + assembleError.call( this, ex.inner ).msg
quit = true
etype = 'custom'
when HMSTATUS.parseError
if SyntaxErrorEx.is ex.inner
console.error printf( M2C(this.msgs.readError.msg, 'red'), ex.file )
se = new SyntaxErrorEx ex, ex.raw
if se.line? and se.col?
msg = printf M2C( this.msgs.parseError.msg[0], 'red' ), se.line, se.col
else if se.line?
msg = printf M2C( this.msgs.parseError.msg[1], 'red' ), se.line
else
msg = M2C @msgs.parseError.msg[2], 'red'
else if ex.inner && ex.inner.line? && ex.inner.col?
msg = printf( M2C( this.msgs.parseError.msg[0], 'red' ), ex.inner.line, ex.inner.col)
else
msg = ex
etype = 'error'
when HMSTATUS.createError
# inner.code could be EPERM, EACCES, etc
msg = printf M2C( this.msgs.createError.msg ), ex.inner.path
etype = 'error'
when HMSTATUS.validateError
msg = printf M2C( @msgs.validateError.msg ), ex.inner.toString()
etype = 'error'
msg: msg # The error message to display
withStack: withStack # Whether to include the stack
quit: quit
etype: etype

View File

@ -1,280 +0,0 @@
/**
Error-handling routines for HackMyResume.
@module cli/error
@license MIT. See LICENSE.md for details.
*/
(function() {
var HMSTATUS = require('hackmycore/src/core/status-codes')
, PKG = require('../../package.json')
, FS = require('fs')
, FCMD = require('hackmycore')
, PATH = require('path')
, WRAP = require('word-wrap')
, M2C = require('hackmycore/src/utils/md2chalk.js')
, chalk = require('chalk')
, extend = require('extend')
, YAML = require('yamljs')
, printf = require('printf')
, SyntaxErrorEx = require('hackmycore/src/utils/syntax-error-ex');
require('string.prototype.startswith');
/**
Error handler for HackMyResume. All errors are handled here.
@class ErrorHandler
*/
var ErrorHandler = module.exports = {
init: function( debug, assert, silent ) {
this.debug = debug;
this.assert = assert;
this.silent = silent;
this.msgs = require('./msg.js').errors;
return this;
},
err: function( ex, shouldExit ) {
// Short-circuit logging output if --silent is on
var o = this.silent ? function() { } : _defaultLog;
// Special case; can probably be removed.
if( ex.pass ) throw ex;
// Load error messages
this.msgs = this.msgs || require('./msg.js').errors;
// Handle packaged HMR exceptions
if( ex.fluenterror ) {
// Output the error message
var objError = assembleError.call( this, ex );
o( this[ 'format_' + objError.etype ]( objError.msg ));
// Output the stack (sometimes)
if( objError.withStack ) {
var stack = ex.stack || (ex.inner && ex.inner.stack);
stack && o( chalk.gray( stack ) );
}
// Quit if necessary
if( ex.quit || objError.quit ) {
this.debug && o(
chalk.cyan('Exiting with error code ' + ex.fluenterror.toString()));
if( this.assert ) { ex.pass = true; throw ex; }
process.exit( ex.fluenterror );
}
}
// Handle raw exceptions
else {
o( ex );
var stackTrace = ex.stack || (ex.inner && ex.inner.stack);
if( stackTrace && this.debug )
o( M2C(ex.stack || ex.inner.stack, 'gray') );
}
},
format_error: function( msg ) {
msg = msg || '';
return chalk.red.bold(
msg.toUpperCase().startsWith('ERROR:') ? msg : 'Error: ' + msg );
},
format_warning: function( brief, msg ) {
return chalk.yellow(brief) + chalk.yellow(msg || '');
},
format_custom: function( msg ) {
return msg;
}
};
function _defaultLog() {
console.log.apply( console.log, arguments );
}
function assembleError( ex ) {
var msg = '', withStack = false, quit = false, etype = 'warning';
if( this.debug ) withStack = true;
switch( ex.fluenterror ) {
case HMSTATUS.themeNotFound:
msg = printf( M2C( this.msgs.themeNotFound.msg, 'yellow' ), ex.data);
break;
case HMSTATUS.copyCSS:
msg = M2C( this.msgs.copyCSS.msg, 'red' );
quit = false;
break;
case HMSTATUS.resumeNotFound:
msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' );
break;
case HMSTATUS.missingCommand:
msg = M2C( this.msgs.missingCommand.msg + " (", 'yellow');
msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? chalk.yellow('or ') : '') +
chalk.yellow.bold(v.toUpperCase());
}).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += chalk.gray(FS.readFileSync(
PATH.resolve(__dirname, '../cli/use.txt'), 'utf8' ));
break;
case HMSTATUS.invalidCommand:
msg = printf( M2C( this.msgs.invalidCommand.msg, 'yellow'), ex.attempted );
break;
case HMSTATUS.resumeNotFoundAlt:
msg = M2C( this.msgs.resumeNotFoundAlt.msg, 'yellow' );
break;
case HMSTATUS.inputOutputParity:
msg = M2C( this.msgs.inputOutputParity.msg );
break;
case HMSTATUS.createNameMissing:
msg = M2C( this.msgs.createNameMissing.msg );
break;
case HMSTATUS.pdfGeneration:
msg = M2C( this.msgs.pdfGeneration.msg, 'bold' );
if( ex.inner ) msg += chalk.red('\n' + ex.inner);
withStack = true; quit = false; etype = 'error';
break;
case HMSTATUS.invalid:
msg = M2C( this.msgs.invalid.msg, 'red' );
etype = 'error';
break;
case HMSTATUS.generateError:
msg = (ex.inner && ex.inner.toString()) || ex;
quit = false;
etype = 'error';
break;
case HMSTATUS.fileSaveError:
msg = printf( M2C( this.msgs.fileSaveError.msg ), (ex.inner || ex).toString() );
etype = 'error';
quit = false;
break;
case HMSTATUS.invalidFormat:
ex.data.forEach(function(d){
msg += printf( M2C( this.msgs.invalidFormat.msg, 'bold' ),
ex.theme.name.toUpperCase(), d.format.toUpperCase());
}, this);
break;
case HMSTATUS.missingParam:
msg = printf( M2C( this.msgs.missingParam.msg ), ex.expected, ex.helper );
break;
case HMSTATUS.invalidHelperUse:
msg = printf( M2C( this.msgs.invalidHelperUse.msg ), ex.helper );
if( ex.error ) {
msg += '\n--> ' + assembleError.call( this, extend( true, {}, ex, {fluenterror: ex.error} )).msg;
//msg += printf( '\n--> ' + M2C( this.msgs.invalidParamCount.msg ), ex.expected );
}
quit = false;
etype = 'warning';
break;
case HMSTATUS.notOnPath:
msg = printf( M2C(this.msgs.notOnPath.msg, 'bold'), ex.engine);
quit = false;
etype = 'error';
break;
case HMSTATUS.readError:
if( !ex.quiet )
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file));
msg = ex.inner.toString();
etype = 'error';
break;
case HMSTATUS.mixedMerge:
msg = M2C( this.msgs.mixedMerge.msg );
quit = false;
break;
case HMSTATUS.invokeTemplate:
msg = M2C( this.msgs.invokeTemplate.msg, 'red' );
msg += M2C( '\n' + WRAP(ex.inner.toString(), { width: 60, indent: ' ' }), 'gray' );
etype = 'custom';
break;
case HMSTATUS.compileTemplate:
etype = 'error';
break;
case HMSTATUS.themeLoad:
msg = M2C( printf( this.msgs.themeLoad.msg, ex.attempted.toUpperCase() ), 'red');
if( ex.inner && ex.inner.fluenterror ) {
msg += M2C('\nError: ', 'red') + assembleError.call( this, ex.inner ).msg;
}
quit = true;
etype = 'custom';
break;
case HMSTATUS.parseError:
if( SyntaxErrorEx.is( ex.inner )) {
console.error( printf( M2C(this.msgs.readError.msg, 'red'), ex.file ) );
var se = new SyntaxErrorEx( ex, ex.raw );
msg = printf( M2C( this.msgs.parseError.msg, 'red' ),
se.line, se.col);
}
else if( ex.inner && ex.inner.line !== undefined && ex.inner.col !== undefined ) {
msg = printf( M2C( this.msgs.parseError.msg, 'red' ),
ex.inner.line, ex.inner.col);
}
else {
msg = ex;
}
etype = 'error';
break;
}
return {
msg: msg, // The error message to display
withStack: withStack, // Whether to include the stack
quit: quit,
etype: etype
};
}
}());

22
src/cli/index.js Normal file
View File

@ -0,0 +1,22 @@
#! /usr/bin/env node
/**
Command-line interface (CLI) for HackMyResume.
@license MIT. See LICENSE.md for details.
@module index.js
*/
try {
require('./main')( process.argv );
}
catch( ex ) {
require('./error').err( ex, true );
}

349
src/cli/main.coffee Normal file
View File

@ -0,0 +1,349 @@
###*
Definition of the `main` function.
@module cli/main
@license MIT. See LICENSE.md for details.
###
HMR = require '../index'
PKG = require '../../package.json'
FS = require 'fs'
EXTEND = require 'extend'
chalk = require 'chalk'
PATH = require 'path'
HMSTATUS = require '../core/status-codes'
HME = require '../core/event-codes'
safeLoadJSON = require '../utils/safe-json-loader'
StringUtils = require '../utils/string.js'
_ = require 'underscore'
OUTPUT = require './out'
PAD = require 'string-padding'
Command = require('commander').Command
M2C = require '../utils/md2chalk'
printf = require 'printf'
_opts = { }
_title = chalk.white.bold('\n*** HackMyResume v' +PKG.version+ ' ***')
_out = new OUTPUT( _opts )
_err = require('./error')
_exitCallback = null
###
A callable implementation of the HackMyResume CLI. Encapsulates the command
line interface as a single method accepting a parameter array.
@alias module:cli/main.main
@param rawArgs {Array} An array of command-line parameters. Will either be
process.argv (in production) or custom parameters (in test).
###
main = module.exports = ( rawArgs, exitCallback ) ->
initInfo = initialize( rawArgs, exitCallback )
args = initInfo.args
# Create the top-level (application) command...
program = new Command('hackmyresume')
.version(PKG.version)
.description(chalk.yellow.bold('*** HackMyResume ***'))
.option('-s --silent', 'Run in silent mode')
.option('--no-color', 'Disable colors')
.option('--color', 'Enable colors')
.option('-d --debug', 'Enable diagnostics', false)
.option('-a --assert', 'Treat warnings as errors', false)
.option('-v --version', 'Show the version')
.allowUnknownOption();
program.jsonArgs = initInfo.options
# Create the NEW command
program
.command 'new'
.arguments '<sources...>'
.option '-f --format <fmt>', 'FRESH or JRS format', 'FRESH'
.alias 'create'
.description 'Create resume(s) in FRESH or JSON RESUME format.'
.action (( sources ) ->
execute.call( this, sources, [], this.opts(), logMsg)
return
)
# Create the VALIDATE command
program
.command('validate')
.arguments('<sources...>')
.description('Validate a resume in FRESH or JSON RESUME format.')
.action((sources) ->
execute.call( this, sources, [], this.opts(), logMsg)
return
)
# Create the CONVERT command
program
.command('convert')
.description('Convert a resume to/from FRESH or JSON RESUME format.')
.action(->
x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg)
return
)
# Create the ANALYZE command
program
.command('analyze')
.arguments('<sources...>')
.description('Analyze one or more resumes.')
.action(( sources ) ->
execute.call( this, sources, [], this.opts(), logMsg)
return
)
# Create the PEEK command
program
.command('peek')
.arguments('<sources...>')
.description('Peek at a resume field or section')
.action(( sources, sectionOrField ) ->
dst = if (sources && sources.length > 1) then [sources.pop()] else []
execute.call( this, sources, dst, this.opts(), logMsg)
return
)
# Create the BUILD command
program
.command('build')
.alias('generate')
.option('-t --theme <theme>', 'Theme name or path')
.option('-n --no-prettify', 'Disable HTML prettification', true)
.option('-c --css <option>', 'CSS linking / embedding')
.option('-p --pdf <engine>', 'PDF generation engine')
.option('--no-sort', 'Sort resume sections by date', false)
.option('--tips', 'Display theme tips and warnings.', false)
.description('Generate resume to multiple formats')
.action(( sources, targets, options ) ->
x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg)
return
)
program.parse( args )
if !program.args.length
throw fluenterror: 4
### Massage command-line args and setup Commander.js. ###
initialize = ( ar, exitCallback ) ->
_exitCallback = exitCallback || process.exit
o = initOptions ar
o.silent || logMsg( _title )
# Emit debug prelude if --debug was specified
if o.debug
_out.log(chalk.cyan('The -d or --debug switch was specified. DEBUG mode engaged.'))
_out.log('')
_out.log(chalk.cyan(PAD(' Platform:',25, null, PAD.RIGHT)) + chalk.cyan.bold( if process.platform == 'win32' then 'windows' else process.platform ))
_out.log(chalk.cyan(PAD(' Node.js:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.version ))
_out.log(chalk.cyan(PAD(' HackMyResume:',25, null, PAD.RIGHT)) + chalk.cyan.bold('v' + PKG.version ))
_out.log(chalk.cyan(PAD(' FRESCA:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies.fresca ))
#_out.log(chalk.cyan(PAD(' fresh-themes:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-themes'] ))
#_out.log(chalk.cyan(PAD(' fresh-jrs-converter:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-jrs-converter'] ))
_out.log('')
_err.init o.debug, o.assert, o.silent
# Handle invalid verbs here (a bit easier here than in commander.js)...
if o.verb && !HMR.verbs[ o.verb ] && !HMR.alias[ o.verb ]
_err.err fluenterror: HMSTATUS.invalidCommand, quit: true, attempted: o.orgVerb, true
# Override the .missingArgument behavior
Command.prototype.missingArgument = (name) ->
_err.err
fluenterror:
if this.name() != 'new'
then HMSTATUS.resumeNotFound
else HMSTATUS.createNameMissing
, true
return
# Override the .helpInformation behavior
Command.prototype.helpInformation = ->
manPage = FS.readFileSync(
PATH.join(__dirname, 'use.txt'), 'utf8' )
return chalk.green.bold(manPage)
return {
args: o.args,
options: o.json
}
### Init options prior to setting up command infrastructure. ###
initOptions = ( ar ) ->
oVerb
verb = ''
args = ar.slice()
cleanArgs = args.slice( 2 )
oJSON
if cleanArgs.length
# Support case-insensitive sub-commands (build, generate, validate, etc)
vidx = _.findIndex cleanArgs, (v) -> v[0] != '-'
if vidx != -1
oVerb = cleanArgs[ vidx ]
verb = args[ vidx + 2 ] = oVerb.trim().toLowerCase()
# Remove --options --opts -o and process separately
optsIdx = _.findIndex cleanArgs, (v) ->
v == '-o' || v == '--options' || v == '--opts'
if optsIdx != -1
optStr = cleanArgs[ optsIdx + 1]
args.splice( optsIdx + 2, 2 )
if optStr && (optStr = optStr.trim())
#var myJSON = JSON.parse(optStr);
if( optStr[0] == '{')
### jshint ignore:start ###
oJSON = eval('(' + optStr + ')') # jshint ignore:line <-- no worky
### jshint ignore:end ###
else
inf = safeLoadJSON( optStr )
if( !inf.ex )
oJSON = inf.json
# TODO: Error handling
# Grab the --debug flag, --silent, --assert and --no-color flags
isDebug = _.some args, (v) -> v == '-d' || v == '--debug'
isSilent = _.some args, (v) -> v == '-s' || v == '--silent'
isAssert = _.some args, (v) -> v == '-a' || v == '--assert'
isMono = _.some args, (v) -> v == '--no-color'
return {
color: !isMono,
debug: isDebug,
silent: isSilent,
assert: isAssert,
orgVerb: oVerb,
verb: verb,
json: oJSON,
args: args
}
### Invoke a HackMyResume verb. ###
execute = ( src, dst, opts, log ) ->
# Create the verb
v = new HMR.verbs[ @name() ]()
# Initialize command-specific options
loadOptions.call( this, opts, this.parent.jsonArgs )
# Set up error/output handling
_opts.errHandler = v
_out.init _opts
# Hook up event notifications
v.on 'hmr:status', -> _out.do.apply _out, arguments
v.on 'hmr:error', -> _err.err.apply _err, arguments
# Invoke the verb using promise syntax
prom = v.invoke.call v, src, dst, _opts, log
prom.then executeSuccess, executeFail
return
### Success handler for verb invocations. Calls process.exit by default ###
executeSuccess = (obj) ->
# Can't call _exitCallback here (process.exit) when PDF is running in BK
#_exitCallback 0; return
### Failure handler for verb invocations. Calls process.exit by default ###
executeFail = (err) ->
finalErrorCode = -1
if err
finalErrorCode = if err.fluenterror then err.fluenterror else err
if _opts.debug
msgs = require('./msg').errors;
logMsg printf M2C( msgs.exiting.msg, 'cyan' ), finalErrorCode
logMsg err.stack if err.stack
_exitCallback finalErrorCode
return
###
Initialize HackMyResume options.
TODO: Options loading is a little hacky, for two reasons:
- Commander.js idiosyncracies
- Need to accept JSON inputs from the command line.
###
loadOptions = ( o, cmdO ) ->
# o and this.opts() seem to be the same (command-specific options)
# Load the specified options file (if any) and apply options
if( cmdO )
o = EXTEND(true, o, cmdO)
# Merge in command-line options
o = EXTEND( true, o, this.opts() )
# Kludge parent-level options until piping issue is resolved
if this.parent.silent != undefined && this.parent.silent != null
o.silent = this.parent.silent
if this.parent.debug != undefined && this.parent.debug != null
o.debug = this.parent.debug
if this.parent.assert != undefined && this.parent.assert != null
o.assert = this.parent.assert
if o.debug
logMsg(chalk.cyan('OPTIONS:') + '\n')
_.each(o, (val, key) ->
logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'),
PAD(key,22,null,PAD.RIGHT), val)
);
logMsg('');
# Cache
EXTEND( true, _opts, o )
return
### Split multiple command-line filenames by the 'TO' keyword ###
splitSrcDest = () ->
params = this.parent.args.filter((j) -> return String.is(j) )
if params.length == 0
throw { fluenterror: HMSTATUS.resumeNotFound, quit: true }
# Find the TO keyword, if any
splitAt = _.findIndex( params, (p) -> return p.toLowerCase() == 'to'; )
# TO can't be the last keyword
if splitAt == params.length - 1 && splitAt != -1
logMsg(chalk.yellow('Please ') +
chalk.yellow.bold('specify an output file') +
chalk.yellow(' for this operation or ') +
chalk.yellow.bold('omit the TO keyword') +
chalk.yellow('.') )
return
return {
src: params.slice(0, if splitAt == -1 then undefined else splitAt ),
dst: if splitAt == -1 then [] else params.slice( splitAt + 1 )
}
### Simple logging placeholder. ###
logMsg = () ->
_opts.silent || console.log.apply( console.log, arguments )

View File

@ -1,331 +0,0 @@
/**
Definition of the `main` function.
@module cli/main
@license MIT. See LICENSE.md for details.
*/
(function(){
var HMR = require( 'hackmycore')
, PKG = require('../../package.json')
, FS = require('fs')
, EXTEND = require('extend')
, chalk = require('chalk')
, PATH = require('path')
, HMSTATUS = require('hackmycore/src/core/status-codes')
, HME = require('hackmycore/src/core/event-codes')
, safeLoadJSON = require('hackmycore/src/utils/safe-json-loader')
, StringUtils = require('hackmycore/src/utils/string.js')
, _ = require('underscore')
, OUTPUT = require('./out')
, PAD = require('string-padding')
, Command = require('commander').Command;
var _opts = { };
var _title = chalk.white.bold('\n*** HackMyResume v' +PKG.version+ ' ***');
var _out = new OUTPUT( _opts );
/**
A callable implementation of the HackMyResume CLI. Encapsulates the command
line interface as a single method accepting a parameter array.
@alias module:cli/main.main
@param rawArgs {Array} An array of command-line parameters. Will either be
process.argv (in production) or custom parameters (in test).
*/
var main = module.exports = function( rawArgs ) {
var initInfo = initialize( rawArgs );
var args = initInfo.args;
// Create the top-level (application) command...
var program = new Command('hackmyresume')
.version(PKG.version)
.description(chalk.yellow.bold('*** HackMyResume ***'))
.option('-s --silent', 'Run in silent mode')
.option('--no-color', 'Disable colors')
.option('--color', 'Enable colors')
.option('-d --debug', 'Enable diagnostics', false)
.option('-a --assert', 'Treat warnings as errors', false)
.option('-v --version', 'Show the version')
.allowUnknownOption();
program.jsonArgs = initInfo.options;
// Create the NEW command
program
.command('new')
.arguments('<sources...>')
.option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
.alias('create')
.description('Create resume(s) in FRESH or JSON RESUME format.')
.action(function( sources ) {
execute.call( this, sources, [], this.opts(), logMsg);
});
// Create the VALIDATE command
program
.command('validate')
.arguments('<sources...>')
.description('Validate a resume in FRESH or JSON RESUME format.')
.action(function(sources) {
execute.call( this, sources, [], this.opts(), logMsg);
});
// Create the CONVERT command
program
.command('convert')
.description('Convert a resume to/from FRESH or JSON RESUME format.')
.action(function() {
var x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg);
});
// Create the ANALYZE command
program
.command('analyze')
.arguments('<sources...>')
.description('Analyze one or more resumes.')
.action(function( sources ) {
execute.call( this, sources, [], this.opts(), logMsg);
});
// Create the PEEK command
program
.command('peek')
.arguments('<sources...>')
.description('Peek at a resume field or section')
.action(function( sources, sectionOrField ) {
var dst = (sources && sources.length > 1) ? [sources.pop()] : [];
execute.call( this, sources, dst, this.opts(), logMsg);
});
// Create the BUILD command
program
.command('build')
.alias('generate')
.option('-t --theme <theme>', 'Theme name or path')
.option('-n --no-prettify', 'Disable HTML prettification', true)
.option('-c --css <option>', 'CSS linking / embedding')
.option('-p --pdf <engine>', 'PDF generation engine')
.option('--no-sort', 'Sort resume sections by date', false)
.option('--tips', 'Display theme tips and warnings.', false)
.description('Generate resume to multiple formats')
.action(function( sources, targets, options ) {
var x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg);
});
program.parse( args );
if (!program.args.length) { throw { fluenterror: 4 }; }
};
/** Massage command-line args and setup Commander.js. */
function initialize( ar ) {
var o = initOptions( ar );
o.silent || logMsg( _title );
// Emit debug prelude if --debug was specified
if( o.debug ) {
_out.log(chalk.cyan('The -d or --debug switch was specified. DEBUG mode engaged.'));
_out.log('');
_out.log(chalk.cyan(PAD(' Platform:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.platform === 'win32' ? 'windows' : process.platform ));
_out.log(chalk.cyan(PAD(' Node.js:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.version ));
_out.log(chalk.cyan(PAD(' HackMyResume:',25, null, PAD.RIGHT)) + chalk.cyan.bold('v' + PKG.version ));
_out.log(chalk.cyan(PAD(' FRESCA:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies.fresca ));
//_out.log(chalk.cyan(PAD(' fresh-themes:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-themes'] ));
//_out.log(chalk.cyan(PAD(' fresh-jrs-converter:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-jrs-converter'] ));
_out.log('');
}
// Handle invalid verbs here (a bit easier here than in commander.js)...
if( o.verb && !HMR.verbs[ o.verb ] && !HMR.alias[ o.verb ] ) {
throw { fluenterror: HMSTATUS.invalidCommand, quit: true,
attempted: o.orgVerb };
}
// Override the .missingArgument behavior
Command.prototype.missingArgument = function(name) {
if( this.name() !== 'new' ) {
throw { fluenterror: HMSTATUS.resumeNotFound, quit: true };
}
};
// Override the .helpInformation behavior
Command.prototype.helpInformation = function() {
var manPage = FS.readFileSync(
PATH.join(__dirname, 'use.txt'), 'utf8' );
return chalk.green.bold(manPage);
};
return {
args: o.args,
options: o.json
};
}
/** Init options prior to setting up command infrastructure. */
function initOptions( ar ) {
var oVerb, verb = '', args = ar.slice(), cleanArgs = args.slice(2), oJSON;
if( cleanArgs.length ) {
// Support case-insensitive sub-commands (build, generate, validate, etc)
var vidx = _.findIndex( cleanArgs, function(v){ return v[0] !== '-'; });
if( vidx !== -1 ) {
oVerb = cleanArgs[ vidx ];
verb = args[ vidx + 2 ] = oVerb.trim().toLowerCase();
}
// Remove --options --opts -o and process separately
var optsIdx = _.findIndex( cleanArgs, function(v){
return v === '-o' || v === '--options' || v === '--opts';
});
if(optsIdx !== -1) {
var optStr = cleanArgs[ optsIdx + 1];
args.splice( optsIdx + 2, 2 );
if( optStr && (optStr = optStr.trim()) ) {
//var myJSON = JSON.parse(optStr);
if( optStr[0] === '{')
oJSON = eval('(' + optStr + ')'); // jshint ignore:line
else {
var inf = safeLoadJSON( optStr );
if( !inf.ex )
oJSON = inf.json;
// TODO: Error handling
}
}
}
}
// Grab the --debug flag
var isDebug = _.some( args, function(v) {
return v === '-d' || v === '--debug';
});
// Grab the --silent flag
var isSilent = _.some( args, function(v) {
return v === '-s' || v === '--silent';
});
return {
debug: isDebug,
silent: isSilent,
orgVerb: oVerb,
verb: verb,
json: oJSON,
args: args
};
}
/** Invoke a HackMyResume verb. */
function execute( src, dst, opts, log ) {
loadOptions.call( this, opts, this.parent.jsonArgs );
var hand = require( './error' );
hand.init( _opts.debug, _opts.assert, _opts.silent );
var v = new HMR.verbs[ this.name() ]();
_opts.errHandler = v;
_out.init( _opts );
v.on( 'hmr:status', function() { _out.do.apply( _out, arguments ); });
v.on( 'hmr:error', function() {
hand.err.apply( hand, arguments );
});
v.invoke.call( v, src, dst, _opts, log );
if( v.errorCode )
process.exit(v.errorCode);
}
/**
Initialize HackMyResume options.
TODO: Options loading is a little hacky, for two reasons:
- Commander.js idiosyncracies
- Need to accept JSON inputs from the command line.
*/
function loadOptions( o, cmdO ) {
// o and this.opts() seem to be the same (command-specific options)
// Load the specified options file (if any) and apply options
if( cmdO )
o = EXTEND(true, o, cmdO);
// Merge in command-line options
o = EXTEND( true, o, this.opts() );
// Kludge parent-level options until piping issue is resolved
if( this.parent.silent !== undefined && this.parent.silent !== null)
o.silent = this.parent.silent;
if( this.parent.debug !== undefined && this.parent.debug !== null)
o.debug = this.parent.debug;
if( this.parent.assert !== undefined && this.parent.assert !== null)
o.assert = this.parent.assert;
if( o.debug ) {
logMsg(chalk.cyan('OPTIONS:') + '\n');
_.each(o, function(val, key) {
logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'),
PAD(key,22,null,PAD.RIGHT), val);
});
logMsg('');
}
// Cache
EXTEND(true, _opts, o);
}
/** Split multiple command-line filenames by the 'TO' keyword */
function splitSrcDest() {
var params = this.parent.args.filter(function(j) { return String.is(j); });
if( params.length === 0 )
throw { fluenterror: HMSTATUS.resumeNotFound, quit: true };
// Find the TO keyword, if any
var splitAt = _.findIndex( params, function(p) {
return p.toLowerCase() === 'to';
});
// TO can't be the last keyword
if( splitAt === params.length - 1 && splitAt !== -1 ) {
logMsg(chalk.yellow('Please ') +
chalk.yellow.bold('specify an output file') +
chalk.yellow(' for this operation or ') +
chalk.yellow.bold('omit the TO keyword') +
chalk.yellow('.') );
return;
}
return {
src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
};
}
/** Simple logging placeholder. */
function logMsg() {
_opts.silent || console.log.apply( console.log, arguments );
}
}());

10
src/cli/msg.coffee Normal file
View File

@ -0,0 +1,10 @@
###*
Message-handling routines for HackMyResume.
@module cli/msg
@license MIT. See LICENSE.md for details.
###
PATH = require 'path'
YAML = require 'yamljs'
module.exports = YAML.load PATH.join __dirname, 'msg.yml'

View File

@ -1,18 +0,0 @@
/**
Message-handling routines for HackMyResume.
@module msg.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var PATH = require('path');
var YAML = require('yamljs');
var cache = module.exports = function() {
return cache ? cache : YAML.load( PATH.join(__dirname, 'msg.yml') );
}();
}());

View File

@ -3,6 +3,8 @@ events:
msg: Invoking **%s** command.
beforeCreate:
msg: Creating new **%s** resume: **%s**
afterCreate:
msg: Creating new **%s** resume: **%s**
afterRead:
msg: Reading **%s** resume: **%s**
beforeTheme:
@ -41,6 +43,8 @@ events:
- "VALID!"
- "INVALID"
- "BROKEN"
- "MISSING"
- "ERROR"
beforePeek:
msg:
- Peeking at **%s** in **%s**
@ -79,7 +83,10 @@ errors:
readError:
msg: Reading **???** resume: **%s**
parseError:
msg: Invalid or corrupt JSON on line %s column %s.
msg:
- Invalid or corrupt JSON on line %s column %s.
- Invalid or corrupt JSON on line %s.
- Invalid or corrupt JSON.
invalidHelperUse:
msg: "**Warning**: Incorrect use of the **%s** theme helper."
fileSaveError:
@ -96,3 +103,9 @@ errors:
msg: "Invalid number of parameters. Expected: **%s**."
missingParam:
msg: The '**%s**' parameter was needed but not supplied.
createError:
msg: Failed to create **'%s'**.
exiting:
msg: Exiting with status code **%s**.
validateError:
msg: "An error occurred during validation:\n%s"

182
src/cli/out.coffee Normal file
View File

@ -0,0 +1,182 @@
###*
Output routines for HackMyResume.
@license MIT. See LICENSE.md for details.
@module cli/out
###
chalk = require('chalk')
HME = require('../core/event-codes')
_ = require('underscore')
M2C = require('../utils/md2chalk.js')
PATH = require('path')
LO = require('lodash')
FS = require('fs')
EXTEND = require('extend')
HANDLEBARS = require('handlebars')
YAML = require('yamljs')
printf = require('printf')
pad = require('string-padding')
dbgStyle = 'cyan';
###* A stateful output module. All HMR console output handled here. ###
module.exports = class OutputHandler
constructor: ( opts ) ->
@init opts
return
init: (opts) ->
@opts = EXTEND( true, @opts || { }, opts )
@msgs = YAML.load(PATH.join( __dirname, 'msg.yml' )).events
return
log: ( msg ) ->
msg = msg || ''
printf = require('printf')
finished = printf.apply( printf, arguments )
@opts.silent || console.log( finished )
do: ( evt ) ->
that = @
L = () -> that.log.apply( that, arguments )
switch evt.sub
when HME.begin
this.opts.debug &&
L( M2C( this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase() )
#when HME.beforeCreate
#L( M2C( this.msgs.beforeCreate.msg, 'green' ), evt.fmt, evt.file )
#break;
when HME.afterCreate
L( M2C( @msgs.beforeCreate.msg, if evt.isError then 'red' else 'green' ), evt.fmt, evt.file )
break;
when HME.beforeTheme
this.opts.debug &&
L( M2C( this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase() )
when HME.afterParse
L( M2C( this.msgs.afterRead.msg, 'gray', 'white.dim'), evt.fmt.toUpperCase(), evt.file )
when HME.beforeMerge
msg = ''
evt.f.reverse().forEach ( a, idx ) ->
msg += printf( (if idx == 0 then @msgs.beforeMerge.msg[0] else @msgs.beforeMerge.msg[1]), a.file )
, @
L( M2C(msg, (if evt.mixed then 'yellow' else 'gray'), 'white.dim') )
when HME.applyTheme
@theme = evt.theme;
numFormats = Object.keys( evt.theme.formats ).length;
L( M2C(this.msgs.applyTheme.msg,
if evt.status == 'error' then 'red' else 'gray',
if evt.status == 'error' then 'bold' else 'white.dim'),
evt.theme.name.toUpperCase(),
numFormats, if numFormats == 1 then '' else 's' )
when HME.end
if evt.cmd == 'build'
themeName = this.theme.name.toUpperCase()
if this.opts.tips && (this.theme.message || this.theme.render)
WRAP = require('word-wrap')
if this.theme.message
L( M2C( this.msgs.afterBuild.msg[0], 'cyan' ), themeName )
L( M2C( this.theme.message, 'white' ))
else if this.theme.render
L( M2C( this.msgs.afterBuild.msg[0], 'cyan'), themeName)
L( M2C( this.msgs.afterBuild.msg[1], 'white'))
when HME.afterGenerate
suffix = ''
if evt.fmt == 'pdf'
if this.opts.pdf
if this.opts.pdf != 'none'
suffix = printf( M2C( this.msgs.afterGenerate.msg[0], if evt.error then 'red' else 'green' ), this.opts.pdf )
else
L( M2C( this.msgs.afterGenerate.msg[1], 'gray' ), evt.fmt.toUpperCase(), evt.file )
return
L( M2C( this.msgs.afterGenerate.msg[2] + suffix, if evt.error then 'red' else 'green' ),
pad( evt.fmt.toUpperCase(),4,null,pad.RIGHT ),
PATH.relative( process.cwd(), evt.file ) );
when HME.beforeAnalyze
L( M2C( this.msgs.beforeAnalyze.msg, 'green' ), evt.fmt, evt.file)
when HME.afterAnalyze
info = evt.info
rawTpl = FS.readFileSync( PATH.join( __dirname, 'analyze.hbs' ), 'utf8')
HANDLEBARS.registerHelper( require('../helpers/console-helpers') )
template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false })
tot = 0
info.keywords.forEach (g) -> tot += g.count
info.keywords.totalKeywords = tot
output = template( info )
@log( chalk.cyan(output) )
when HME.beforeConvert
L( M2C( this.msgs.beforeConvert.msg, 'green' ),
evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt
);
when HME.afterInlineConvert
L( M2C( this.msgs.afterInlineConvert.msg, 'gray', 'white.dim' ),
evt.file, evt.fmt );
when HME.afterValidate
style = 'red'
adj = ''
msgs = @msgs.afterValidate.msg;
switch evt.status
when 'valid' then style = 'green'; adj = msgs[1]
when 'invalid' then style = 'yellow'; adj = msgs[2]
when 'broken' then style = 'red'; adj = msgs[3]
when 'missing' then style = 'red'; adj = msgs[4]
when 'unknown' then style = 'red'; adj = msgs[5]
evt.schema = evt.schema.replace('jars','JSON Resume').toUpperCase()
L(M2C( msgs[0], 'white' ) + chalk[style].bold(adj), evt.file, evt.schema)
if evt.violations
_.each evt.violations, (err,idx) ->
L( chalk.yellow.bold('--> ') +
chalk.yellow(err.field.replace('data.','resume.').toUpperCase() +
' ' + err.message))
return
, @
return
when HME.afterPeek
sty = if evt.error then 'red' else ( if evt.target != undefined then 'green' else 'yellow' )
# "Peeking at 'someKey' in 'someFile'."
if evt.requested
L(M2C(this.msgs.beforePeek.msg[0], sty), evt.requested, evt.file)
else
L(M2C(this.msgs.beforePeek.msg[1], sty), evt.file)
# If the key was present, print it
if evt.target != undefined and !evt.error
console.dir( evt.target, { depth: null, colors: true } )
# If the key was not present, but no error occurred, print it
else if !evt.error
L M2C( this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file
else if evt.error
L chalk.red( evt.error.inner.inner )

View File

@ -1,229 +0,0 @@
/**
Output routines for HackMyResume.
@license MIT. See LICENSE.md for details.
@module out.js
*/
(function() {
var chalk = require('chalk')
, HME = require('hackmycore/src/core/event-codes')
, _ = require('underscore')
, Class = require('hackmycore/src/utils/class.js')
, M2C = require('hackmycore/src/utils/md2chalk.js')
, PATH = require('path')
, LO = require('lodash')
, FS = require('fs')
, EXTEND = require('extend')
, HANDLEBARS = require('handlebars')
, YAML = require('yamljs')
, printf = require('printf')
, pad = require('string-padding')
, dbgStyle = 'cyan';
/**
A stateful output module. All HMR console output handled here.
*/
var OutputHandler = module.exports = Class.extend({
init: function( opts ) {
this.opts = EXTEND( true, this.opts || { }, opts );
this.msgs = YAML.load(PATH.join( __dirname, 'msg.yml' )).events;
},
log: function( msg ) {
msg = msg || '';
var printf = require('printf');
var finished = printf.apply( printf, arguments );
this.opts.silent || console.log( finished );
},
do: function( evt ) {
var that = this;
function L() {
that.log.apply( that, arguments );
}
switch( evt.sub ) {
case HME.begin:
this.opts.debug &&
L( M2C( this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase() );
break;
case HME.error:
break;
case HME.beforeCreate:
L( M2C( this.msgs.beforeCreate.msg, 'green' ), evt.fmt, evt.file );
break;
case HME.beforeRead:
break;
case HME.afterRead:
break;
case HME.beforeTheme:
this.opts.debug &&
L( M2C( this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase() );
break;
case HME.afterParse:
L(
M2C( this.msgs.afterRead.msg, 'gray', 'white.dim'), evt.fmt.toUpperCase(), evt.file
);
break;
case HME.afterTheme:
break;
case HME.beforeMerge:
var msg = '';
evt.f.reverse().forEach( function( a, idx ) {
msg += printf(
((idx === 0) ?
this.msgs.beforeMerge.msg[0] :
this.msgs.beforeMerge.msg[1] ), a.file
);
}, this);
L( M2C(msg, evt.mixed ? 'yellow' : 'gray', 'white.dim') );
break;
case HME.afterMerge:
break;
case HME.applyTheme:
this.theme = evt.theme;
var numFormats = Object.keys( evt.theme.formats ).length;
L( M2C(this.msgs.applyTheme.msg, evt.status === 'error' ? 'red' : 'gray', evt.status === 'error' ? 'bold' : 'white.dim'),
evt.theme.name.toUpperCase(),
numFormats, ( numFormats === 1 ? '' : 's') );
break;
case HME.end:
if( evt.cmd === 'build' ) {
var themeName = this.theme.name.toUpperCase();
if( this.opts.tips && (this.theme.message || this.theme.render) ) {
var WRAP = require('word-wrap');
if( this.theme.message ) {
L( M2C( this.msgs.afterBuild.msg[0], 'cyan' ), themeName );
L( M2C( this.theme.message, 'white' ));
}
else if ( this.theme.render ) {
L( M2C( this.msgs.afterBuild.msg[0], 'cyan'), themeName);
L( M2C( this.msgs.afterBuild.msg[1], 'white'));
}
}
}
break;
case HME.afterGenerate:
var suffix = '';
if( evt.fmt === 'pdf' ) {
if( this.opts.pdf ) {
if( this.opts.pdf !== 'none' ) {
suffix = printf( M2C( this.msgs.afterGenerate.msg[0], evt.error ? 'red' : 'green' ), this.opts.pdf );
}
else {
L( M2C( this.msgs.afterGenerate.msg[1], 'gray' ),
evt.fmt.toUpperCase(), evt.file );
return;
}
}
}
L( M2C( this.msgs.afterGenerate.msg[2] + suffix, evt.error ? 'red' : 'green' ),
pad(evt.fmt.toUpperCase(),4,null,pad.RIGHT),
PATH.relative(process.cwd(), evt.file ));
break;
case HME.beforeAnalyze:
L( M2C( this.msgs.beforeAnalyze.msg, 'green' ), evt.fmt, evt.file);
break;
case HME.afterAnalyze:
var info = evt.info;
var rawTpl = FS.readFileSync( PATH.join( __dirname, 'analyze.hbs' ), 'utf8');
HANDLEBARS.registerHelper( require('../helpers/console-helpers') );
var template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false });
var tot = 0;
info.keywords.forEach(function(g) { tot += g.count; });
info.keywords.totalKeywords = tot;
var output = template( info );
this.log( chalk.cyan(output) );
break;
case HME.beforeConvert:
L( M2C( this.msgs.beforeConvert.msg, 'green' ),
evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt
);
break;
case HME.afterInlineConvert:
L( M2C( this.msgs.afterInlineConvert.msg, 'gray', 'white.dim' ),
evt.file, evt.fmt );
break;
case HME.afterValidate:
var style = evt.isValid ? 'green' : 'yellow';
L(
M2C( this.msgs.afterValidate.msg[0], 'white' ) +
chalk[style].bold( evt.isValid ?
this.msgs.afterValidate.msg[1] :
this.msgs.afterValidate.msg[2] ),
evt.file, evt.fmt
);
if( evt.errors ) {
_.each(evt.errors, function(err,idx) {
L( chalk.yellow.bold('--> ') +
chalk.yellow(err.field.replace('data.','resume.').toUpperCase() + ' ' +
err.message) );
}, this);
}
break;
case HME.beforePeek:
// if( evt.target )
// L(M2C(this.msgs.beforePeek.msg[0], evt.isError ? 'red' : 'green'), evt.target, evt.file);
// else
// L(M2C(this.msgs.beforePeek.msg[1], evt.isError ? 'red' : 'green'), evt.file);
break;
case HME.afterPeek:
var sty = evt.error ? 'red' : ( evt.target !== undefined ? 'green' : 'yellow' );
if( evt.requested )
L(M2C(this.msgs.beforePeek.msg[0], sty), evt.requested, evt.file);
else
L(M2C(this.msgs.beforePeek.msg[1], sty), evt.file);
if( evt.target !== undefined )
console.dir( evt.target, { depth: null, colors: true } );
else if( !evt.error )
L(M2C( this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file);
break;
}
}
});
}());

View File

@ -0,0 +1,53 @@
###*
Definition of the AbstractResume class.
@license MIT. See LICENSE.md for details.
@module core/abstract-resume
###
_ = require 'underscore'
__ = require 'lodash'
FluentDate = require('./fluent-date')
class AbstractResume
###*
Compute the total duration of the work history.
@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.
###
duration: (collKey, startKey, endKey, unit) ->
unit = unit || 'years'
hist = __.get @, collKey
return 0 if !hist or !hist.length
# BEGIN CODE DUPLICATION --> src/inspectors/gap-inspector.coffee (TODO)
# Convert the candidate's employment history to an array of dates,
# where each element in the array is a start date or an end date of a
# job -- it doesn't matter which.
new_e = hist.map ( job ) ->
obj = _.pick( job, [startKey, endKey] )
# Synthesize an end date if this is a "current" gig
obj[endKey] = 'current' if !_.has obj, endKey
if obj && (obj[startKey] || obj[endKey])
obj = _.pairs obj
obj[0][1] = FluentDate.fmt( obj[0][1] )
if obj.length > 1
obj[1][1] = FluentDate.fmt( obj[1][1] )
obj
# Flatten the array, remove empties, and sort
new_e = _.filter _.flatten( new_e, true ), (v) ->
return v && v.length && v[0] && v[0].length
return 0 if !new_e or !new_e.length
new_e = _.sortBy new_e, ( elem ) -> return elem[1].unix()
# END CODE DUPLICATION
firstDate = _.first( new_e )[1];
lastDate = _.last( new_e )[1];
lastDate.diff firstDate, unit
module.exports = AbstractResume

View File

@ -0,0 +1,18 @@
###
Event code definitions.
@module core/default-formats
@license MIT. See LICENSE.md for details.
###
###* Supported resume formats. ###
module.exports = [
{ name: 'html', ext: 'html', gen: new (require('../generators/html-generator'))() },
{ name: 'txt', ext: 'txt', gen: new (require('../generators/text-generator'))() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../generators/word-generator'))() },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new (require('../generators/html-pdf-cli-generator'))() },
{ name: 'png', ext: 'png', fmt: 'html', is: false, gen: new (require('../generators/html-png-generator'))() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new (require('../generators/markdown-generator'))() },
{ name: 'json', ext: 'json', gen: new (require('../generators/json-generator'))() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../generators/json-yaml-generator'))() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../generators/latex-generator'))() }
]

View File

@ -0,0 +1,13 @@
###
Event code definitions.
@module core/default-options
@license MIT. See LICENSE.md for details.
###
module.exports =
theme: 'modern'
prettify: # ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2
unformatted: ['em','strong']
max_char: 80, # ← See lib/html.js in above-linked repo
# wrap_line_length: 120, ← Don't use this

77
src/core/empty-jrs.json Normal file
View File

@ -0,0 +1,77 @@
{
"basics": {
"name": "",
"label": "",
"picture": "",
"email": "",
"phone": "",
"degree": "",
"website": "",
"summary": "",
"location": {
"address": "",
"postalCode": "",
"city": "",
"countryCode": "",
"region": ""
},
"profiles": [{
"network": "",
"username": "",
"url": ""
}]
},
"work": [{
"company": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [
""
]
}],
"awards": [{
"title": "",
"date": "",
"awarder": "",
"summary": ""
}],
"education": [{
"institution": "",
"area": "",
"studyType": "",
"startDate": "",
"endDate": "",
"gpa": "",
"courses": [ "" ]
}],
"publications": [{
"name": "",
"publisher": "",
"releaseDate": "",
"website": "",
"summary": ""
}],
"volunteer": [{
"organization": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [ "" ]
}],
"skills": [{
"name": "",
"level": "",
"keywords": [""]
}]
}

View File

@ -0,0 +1,38 @@
###
Event code definitions.
@module core/event-codes
@license MIT. See LICENSE.md for details.
###
module.exports =
error: -1
success: 0
begin: 1
end: 2
beforeRead: 3
afterRead: 4
beforeCreate: 5
afterCreate: 6
beforeTheme: 7
afterTheme: 8
beforeMerge: 9
afterMerge: 10
beforeGenerate: 11
afterGenerate: 12
beforeAnalyze: 13
afterAnalyze: 14
beforeConvert: 15
afterConvert: 16
verifyOutputs: 17
beforeParse: 18
afterParse: 19
beforePeek: 20
afterPeek: 21
beforeInlineConvert: 22
afterInlineConvert: 23
beforeValidate: 24
afterValidate: 25
beforeWrite: 26
afterWrite: 27
applyTheme: 28

View File

@ -0,0 +1,77 @@
###*
The HackMyResume date representation.
@license MIT. See LICENSE.md for details.
@module core/fluent-date
###
moment = require 'moment'
require('../utils/string')
###*
Create a FluentDate from a string or Moment date object. There are a few date
formats to be aware of here.
1. The words "Present" and "Now", referring to the current date
2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10")
3. Year-and-month only ("2015-04")
4. Year-only "YYYY" ("2015")
5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008")
6. Empty dates ("", " ")
7. Any other date format that Moment.js can parse from
Note: Moment can transparently parse all or most of these, without requiring us
to specify a date format...but for maximum parsing safety and to avoid Moment
deprecation warnings, it's recommended to either a) explicitly specify the date
format or b) use an ISO format. For clarity, we handle these cases explicitly.
@class FluentDate
###
class FluentDate
constructor: (dt) ->
@rep = this.fmt dt
@isCurrent: (dt) ->
!dt || (String.is(dt) and /^(present|now|current)$/.test(dt))
months = {}
abbr = {}
moment.months().forEach((m,idx) -> months[m.toLowerCase()] = idx+1 )
moment.monthsShort().forEach((m,idx) -> abbr[m.toLowerCase()]=idx+1 )
abbr.sept = 9
module.exports = FluentDate
FluentDate.fmt = ( dt, throws ) ->
throws = (throws == undefined || throws == null) || throws
if typeof dt == 'string' or dt instanceof String
dt = dt.toLowerCase().trim()
if /^(present|now|current)$/.test(dt) # "Present", "Now"
return moment()
else if /^\D+\s+\d{4}$/.test(dt) # "Mar 2015"
parts = dt.split(' ');
month = (months[parts[0]] || abbr[parts[0]]);
temp = parts[1] + '-' + (month < 10 ? '0' + month : month.toString());
return moment temp, 'YYYY-MM'
else if /^\d{4}-\d{1,2}$/.test(dt) # "2015-03", "1998-4"
return moment dt, 'YYYY-MM'
else if /^\s*\d{4}\s*$/.test(dt) # "2015"
return moment dt, 'YYYY'
else if /^\s*$/.test(dt) # "", " "
return moment()
else
mt = moment dt
if mt.isValid()
return mt
if throws
throw 'Invalid date format encountered.'
return null
else
if !dt
return moment()
else if dt.isValid and dt.isValid()
return dt
if throws
throw 'Unknown date object encountered.'
return null

View File

@ -0,0 +1,410 @@
###*
Definition of the FRESHResume class.
@license MIT. See LICENSE.md for details.
@module core/fresh-resume
###
FS = require 'fs'
extend = require 'extend'
validator = require 'is-my-json-valid'
_ = require 'underscore'
__ = require 'lodash'
PATH = require 'path'
moment = require 'moment'
XML = require 'xml-escape'
MD = require 'marked'
CONVERTER = require 'fresh-jrs-converter'
JRSResume = require './jrs-resume'
FluentDate = require './fluent-date'
AbstractResume = require './abstract-resume'
###*
A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
object is an instantiation of that JSON decorated with utility methods.
@constructor
###
class FreshResume extends AbstractResume
###* Initialize the the FreshResume from JSON string data. ###
parse: ( stringData, opts ) ->
@imp = @imp ? raw: stringData
this.parseJSON JSON.parse( stringData ), opts
###*
Initialize the FreshResume from JSON.
Open and parse the specified FRESH resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
###
parseJSON: ( rep, opts ) ->
# Ignore any element with the 'ignore: true' designator.
that = @
traverse = require 'traverse'
ignoreList = []
scrubbed = traverse( rep ).map ( x ) ->
if !@isLeaf && @node.ignore
if @node.ignore == true || this.node.ignore == 'true'
ignoreList.push this.node
@remove()
# Now apply the resume representation onto this object
extend( true, @, scrubbed );
# If the resume has already been processed, then we are being called from
# the .dupe method, and there's no need to do any post processing
if !@imp?.processed
# Set up metadata TODO: Clean up metadata on the object model.
opts = opts || { }
if opts.imp == undefined || opts.imp
@imp = @imp || { }
@imp.title = (opts.title || @imp.title) || @name
unless @imp.raw
@imp.raw = JSON.stringify rep
@imp.processed = true
# Parse dates, sort dates, and calculate computed values
(opts.date == undefined || opts.date) && _parseDates.call( this );
(opts.sort == undefined || opts.sort) && this.sort();
(opts.compute == undefined || opts.compute) && (@computed = {
numYears: this.duration(),
keywords: this.keywords()
});
@
###* Save the sheet to disk (for environments that have disk access). ###
save: ( filename ) ->
@imp.file = filename || @imp.file
FS.writeFileSync @imp.file, @stringify(), 'utf8'
@
###*
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
###
saveAs: ( filename, format ) ->
if format != 'JRS'
@imp.file = filename || @imp.file
FS.writeFileSync @imp.file, @stringify(), 'utf8'
else
newRep = CONVERTER.toJRS this
FS.writeFileSync filename, JRSResume.stringify( newRep ), 'utf8'
@
###*
Duplicate this FreshResume instance.
This method first extend()s this object onto an empty, creating a deep copy,
and then passes the result into a new FreshResume instance via .parseJSON.
We do it this way to create a true clone of the object without re-running any
of the associated processing.
###
dupe: () ->
jso = extend true, { }, @
rnew = new FreshResume()
rnew.parseJSON jso, { }
rnew
###*
Convert this object to a JSON string, sanitizing meta-properties along the
way.
###
stringify: () -> FreshResume.stringify @
###*
Create a copy of this resume in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder).
TODO: Move this out of FRESHResume.
###
transformStrings: ( filt, transformer ) ->
ret = this.dupe()
trx = require '../utils/string-transformer'
trx ret, filt, transformer
###*
Create a copy of this resume in which all fields have been interpreted as
Markdown.
###
markdownify: () ->
MDIN = ( txt ) ->
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '')
trx = ( key, val ) ->
if key == 'summary'
return MD val
MDIN(val)
return @transformStrings ['skills','url','start','end','date'], trx
###*
Create a copy of this resume in which all fields have been interpreted as
Markdown.
###
xmlify: () ->
trx = (key, val) -> XML val
return @transformStrings [], trx
###* Return the resume format. ###
format: () -> 'FRESH'
###*
Return internal metadata. Create if it doesn't exist.
###
i: () -> this.imp = this.imp || { }
###* Return a unique list of all keywords across all skills. ###
keywords: () ->
flatSkills = []
if @skills
if @skills.sets
flatSkills = @skills.sets.map((sk) -> sk.skills ).reduce( (a,b) -> a.concat(b) )
else if @skills.list
flatSkills = flatSkills.concat( this.skills.list.map (sk) -> return sk.name )
flatSkills = _.uniq flatSkills
flatSkills
###*
Reset the sheet to an empty state. TODO: refactor/review
###
clear: ( clearMeta ) ->
clearMeta = ((clearMeta == undefined) && true) || clearMeta
delete this.imp if clearMeta
delete this.computed # Don't use Object.keys() here
delete this.employment
delete this.service
delete this.education
delete this.recognition
delete this.reading
delete this.writing
delete this.interests
delete this.skills
delete this.social
###*
Get a safe count of the number of things in a section.
###
count: ( obj ) ->
return 0 if !obj
return obj.history.length if obj.history
return obj.sets.length if obj.sets
obj.length || 0;
###* Add work experience to the sheet. ###
add: ( moniker ) ->
defSheet = FreshResume.default()
newObject =
if defSheet[moniker].history
then $.extend( true, {}, defSheet[ moniker ].history[0] )
else
if moniker == 'skills'
then $.extend( true, {}, defSheet.skills.sets[0] )
else $.extend( true, {}, defSheet[ moniker ][0] )
@[ moniker ] = @[ moniker ] || []
if @[ moniker ].history
@[ moniker ].history.push newObject
else if moniker == 'skills'
@skills.sets.push newObject
else
@[ moniker ].push newObject
newObject
###*
Determine if the sheet includes a specific social profile (eg, GitHub).
###
hasProfile: ( socialNetwork ) ->
socialNetwork = socialNetwork.trim().toLowerCase()
@social && _.some @social, (p) ->
p.network.trim().toLowerCase() == socialNetwork
###* Return the specified network profile. ###
getProfile: ( socialNetwork ) ->
socialNetwork = socialNetwork.trim().toLowerCase()
@social && _.find @social, (sn) ->
sn.network.trim().toLowerCase() == socialNetwork
###*
Return an array of profiles for the specified network, for when the user
has multiple eg. GitHub accounts.
###
getProfiles: ( socialNetwork ) ->
socialNetwork = socialNetwork.trim().toLowerCase()
@social && _.filter @social, (sn) ->
sn.network.trim().toLowerCase() == socialNetwork
###* Determine if the sheet includes a specific skill. ###
hasSkill: ( skill ) ->
skill = skill.trim().toLowerCase()
@skills && _.some @skills, (sk) ->
sk.keywords && _.some sk.keywords, (kw) ->
kw.trim().toLowerCase() == skill
###* Validate the sheet against the FRESH Resume schema. ###
isValid: ( info ) ->
schemaObj = require 'fresca'
validator = require 'is-my-json-valid'
validate = validator( schemaObj, { # See Note [1].
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
})
ret = validate @
if !ret
this.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
ret
duration: (unit) ->
super('employment.history', 'start', 'end', unit)
###*
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
###
sort: () ->
byDateDesc = (a,b) ->
if a.safe.start.isBefore(b.safe.start)
then 1
else ( if a.safe.start.isAfter(b.safe.start) then -1 else 0 )
sortSection = ( key ) ->
ar = __.get this, key
if ar && ar.length
datedThings = obj.filter (o) -> o.start
datedThings.sort( byDateDesc );
sortSection 'employment.history'
sortSection 'education.history'
sortSection 'service.history'
sortSection 'projects'
@writing && @writing.sort (a, b) ->
if a.safe.date.isBefore b.safe.date
then 1
else ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0
###*
Get the default (starter) sheet.
###
FreshResume.default = () ->
new FreshResume().parseJSON require('fresh-resume-starter').fresh
###*
Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
along the way.
###
FreshResume.stringify = ( obj ) ->
replacer = ( key,value ) -> # Exclude these keys from stringification
exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar']
return if _.some( exKeys, (val) -> key.trim() == val )
then undefined else value
JSON.stringify obj, replacer, 2
###*
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
###
_parseDates = () ->
_fmt = require('./fluent-date').fmt
that = @
# TODO: refactor recursion
replaceDatesInObject = ( obj ) ->
return if !obj
if Object.prototype.toString.call( obj ) == '[object Array]'
obj.forEach (elem) -> replaceDatesInObject( elem )
return
else if typeof obj == 'object'
if obj._isAMomentObject || obj.safe
return
Object.keys( obj ).forEach (key) -> replaceDatesInObject obj[key]
['start','end','date'].forEach (val) ->
if (obj[val] != undefined) && (!obj.safe || !obj.safe[val])
obj.safe = obj.safe || { }
obj.safe[ val ] = _fmt obj[val]
if obj[val] && (val == 'start') && !obj.end
obj.safe.end = _fmt 'current'
return
return
Object.keys( this ).forEach (member) ->
replaceDatesInObject(that[member])
return
return
###* 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}$/
#

209
src/core/fresh-theme.coffee Normal file
View File

@ -0,0 +1,209 @@
###*
Definition of the FRESHTheme class.
@module core/fresh-theme
@license MIT. See LICENSE.md for details.
###
FS = require 'fs'
validator = require 'is-my-json-valid'
_ = require 'underscore'
PATH = require 'path'
parsePath = require 'parse-filepath'
pathExists = require('path-exists').sync
EXTEND = require 'extend'
HMSTATUS = require './status-codes'
moment = require 'moment'
loadSafeJson = require '../utils/safe-json-loader'
READFILES = require 'recursive-readdir-sync'
### A representation of a FRESH theme asset.
@class FRESHTheme ###
class FRESHTheme
### Open and parse the specified theme. ###
open: ( themeFolder ) ->
@folder = themeFolder
# Open the [theme-name].json file; should have the same name as folder
pathInfo = parsePath themeFolder
# Set up a formats hash for the theme
formatsHash = { }
# Load the theme
themeFile = PATH.join themeFolder, 'theme.json'
themeInfo = loadSafeJson themeFile
if themeInfo.ex
throw
fluenterror:
if themeInfo.ex.operation == 'parse'
then HMSTATUS.parseError
else HMSTATUS.readError
inner: themeInfo.ex.inner
that = this
# Move properties from the theme JSON file to the theme object
EXTEND true, @, themeInfo.json
# Check for an "inherits" entry in the theme JSON.
if @inherits
cached = { }
_.each @inherits, (th, key) ->
themesFolder = require.resolve 'fresh-themes'
d = parsePath( themeFolder ).dirname
themePath = PATH.join d, th
cached[ th ] = cached[th] || new FRESHTheme().open( themePath )
formatsHash[ key ] = cached[ th ].getFormat( key )
# Load theme files
formatsHash = _load.call @, formatsHash
# Cache
@formats = formatsHash
# Set the official theme name
@name = parsePath( @folder ).name
@
### Determine if the theme supports the specified output format. ###
hasFormat: ( fmt ) -> _.has @formats, fmt
### Determine if the theme supports the specified output format. ###
getFormat: ( fmt ) -> @formats[ fmt ]
### Load and parse theme source files. ###
_load = (formatsHash) ->
that = @
major = false
tplFolder = PATH.join @folder, 'src'
copyOnly = ['.ttf','.otf', '.png','.jpg','.jpeg','.pdf']
# 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.
fmts = READFILES(tplFolder).map (absPath) ->
_loadOne.call @, absPath, formatsHash, tplFolder
, @
# Now, get all the CSS files...
@cssFiles = fmts.filter (fmt) -> fmt and (fmt.ext == 'css')
# For each CSS file, get its corresponding HTML file. It's possible that
# a theme can have a CSS file but *no* HTML file, as when a theme author
# creates a pure CSS override of an existing theme.
@cssFiles.forEach (cssf) ->
idx = _.findIndex fmts, ( fmt ) ->
fmt && fmt.pre == cssf.pre && fmt.ext == 'html'
cssf.major = false
if idx > -1
fmts[ idx ].css = cssf.data
fmts[ idx ].cssPath = cssf.path
else
if that.inherits
# Found a CSS file without an HTML file in a theme that inherits
# from another theme. This is the override CSS file.
that.overrides = { file: cssf.path, data: cssf.data }
formatsHash
### Load a single theme file. ###
_loadOne = ( absPath, formatsHash, tplFolder ) ->
pathInfo = parsePath absPath
absPathSafe = absPath.trim().toLowerCase()
outFmt = ''
act = 'copy'
isPrimary = false
# If this is an "explicit" theme, all files of importance are specified in
# the "transform" section of the theme.json file.
if @explicit
outFmt = _.find Object.keys( @formats ), ( fmtKey ) ->
fmtVal = @formats[ fmtKey ]
_.some fmtVal.transform, (fpath) ->
absPathB = PATH.join( @folder, fpath ).trim().toLowerCase()
absPathB == absPathSafe
, @
, @
act = 'transform' if outFmt
if !outFmt
# If this file lives in a specific format folder within the theme,
# such as "/latex" or "/html", then that format is the implicit output
# format for all files within the folder
portion = pathInfo.dirname.replace tplFolder,''
if portion && portion.trim()
return if portion[1] == '_'
reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig
res = reg.exec( portion )
if res
if res[1] != 'partials'
outFmt = res[1]
act = 'transform' if !@explicit
else
@partials = @partials || []
@partials.push( { name: pathInfo.name, path: absPath } )
return null
# Otherwise, the output format is inferred from the filename, as in
# compact-[outputformat].[extension], for ex, compact-pdf.html
if !outFmt
idx = pathInfo.name.lastIndexOf '-'
outFmt = if idx == -1 then pathInfo.name else pathInfo.name.substr idx+1
act = 'transform' if !@explicit
defFormats = require './default-formats'
isPrimary = _.some defFormats, (form) ->
form.name == outFmt and pathInfo.extname != '.css'
# Make sure we have a valid formatsHash
formatsHash[ outFmt ] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
}
# Move symlink descriptions from theme.json to the format
if @formats?[ outFmt ]?.symLinks
formatsHash[ outFmt ].symLinks = @formats[ outFmt ].symLinks
# Create the file representation object
obj =
action: act
primary: isPrimary
path: absPath
orgPath: PATH.relative tplFolder, absPath
ext: pathInfo.extname.slice 1
title: friendlyName outFmt
pre: outFmt
# outFormat: outFmt || pathInfo.name,
data: FS.readFileSync absPath, 'utf8'
css: null
# Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj )
obj
### Return a more friendly name for certain formats. ###
friendlyName = ( val ) ->
val = (val && val.trim().toLowerCase()) || ''
friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }
friendly[val] || val
module.exports = FRESHTheme

337
src/core/jrs-resume.coffee Normal file
View File

@ -0,0 +1,337 @@
###*
Definition of the JRSResume class.
@license MIT. See LICENSE.md for details.
@module core/jrs-resume
###
FS = require('fs')
extend = require('extend')
validator = require('is-my-json-valid')
_ = require('underscore')
PATH = require('path')
MD = require('marked')
CONVERTER = require('fresh-jrs-converter')
moment = require('moment')
AbstractResume = require('./abstract-resume')
###*
A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
is an instantiation of that JSON decorated with utility methods.
@class JRSResume
###
class JRSResume extends AbstractResume
###* Initialize the the JSResume from string. ###
parse: ( stringData, opts ) ->
@imp = @imp ? raw: stringData
this.parseJSON JSON.parse( stringData ), opts
###*
Initialize the JRSResume object from JSON.
Open and parse the specified JRS resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
###
parseJSON: ( rep, opts ) ->
opts = opts || { };
# Ignore any element with the 'ignore: true' designator.
that = this
traverse = require 'traverse'
ignoreList = []
scrubbed = traverse( rep ).map ( x ) ->
if !@isLeaf && @node.ignore
if @node.ignore == true || this.node.ignore == 'true'
ignoreList.push @node
@remove()
# Extend resume properties onto ourself.
extend true, this, scrubbed
# Set up metadata
if !@imp?.processed
# Set up metadata TODO: Clean up metadata on the object model.
opts = opts || { }
if opts.imp == undefined || opts.imp
@imp = @imp || { }
@imp.title = (opts.title || @imp.title) || @basics.name
unless @imp.raw
@imp.raw = JSON.stringify rep
@imp.processed = true
# Parse dates, sort dates, and calculate computed values
(opts.date == undefined || opts.date) && _parseDates.call( this )
(opts.sort == undefined || opts.sort) && this.sort()
if opts.compute == undefined || opts.compute
@basics.computed =
numYears: this.duration()
keywords: this.keywords()
@
###* Save the sheet to disk (for environments that have disk access). ###
save: ( filename ) ->
@imp.file = filename || @imp.file
FS.writeFileSync @imp.file, @stringify( this ), 'utf8'
@
###* Save the sheet to disk in a specific format, either FRESH or JRS. ###
saveAs: ( filename, format ) ->
if format == 'JRS'
@imp.file = filename || @imp.file;
FS.writeFileSync( @imp.file, @stringify(), 'utf8' );
else
newRep = CONVERTER.toFRESH @
stringRep = CONVERTER.toSTRING newRep
FS.writeFileSync filename, stringRep, 'utf8'
@
###* Return the resume format. ###
format: () -> 'JRS'
stringify: () -> JRSResume.stringify( @ )
###* Return a unique list of all keywords across all skills. ###
keywords: () ->
flatSkills = []
if @skills && this.skills.length
@skills.forEach ( s ) -> flatSkills = _.union flatSkills, s.keywords
flatSkills
###*
Return internal metadata. Create if it doesn't exist.
JSON Resume v0.0.0 doesn't allow additional properties at the root level,
so tuck this into the .basic sub-object.
###
i: () ->
@imp = @imp ? { }
###* Reset the sheet to an empty state. ###
clear = ( clearMeta ) ->
clearMeta = ((clearMeta == undefined) && true) || clearMeta;
delete this.imp if clearMeta
delete this.basics.computed # Don't use Object.keys() here
delete this.work
delete this.volunteer
delete this.education
delete this.awards
delete this.publications
delete this.interests
delete this.skills
delete this.basics.profiles
###* Add work experience to the sheet. ###
add: ( moniker ) ->
defSheet = JRSResume.default()
newObject = $.extend( true, {}, defSheet[ moniker ][0] )
this[ moniker ] = this[ moniker ] || []
this[ moniker ].push( newObject )
newObject
###* Determine if the sheet includes a specific social profile (eg, GitHub). ###
hasProfile: ( socialNetwork ) ->
socialNetwork = socialNetwork.trim().toLowerCase()
return @basics.profiles && _.some @basics.profiles, (p) ->
return p.network.trim().toLowerCase() == socialNetwork
###* Determine if the sheet includes a specific skill. ###
hasSkill: ( skill ) ->
skill = skill.trim().toLowerCase()
return this.skills && _.some this.skills, (sk) ->
return sk.keywords && _.some sk.keywords, (kw) ->
kw.trim().toLowerCase() == skill
###* Validate the sheet against the JSON Resume schema. ###
isValid: ( ) -> # TODO: ↓ fix this path ↓
schema = FS.readFileSync PATH.join( __dirname, 'resume.json' ), 'utf8'
schemaObj = JSON.parse schema
validator = require 'is-my-json-valid'
validate = validator( schemaObj, { # Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
temp = @imp
delete @imp
ret = validate @
@imp = temp
if !ret
@imp = @imp || { };
@imp.validationErrors = validate.errors;
ret
duration: (unit) ->
super('work', 'startDate', 'endDate', unit)
###*
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
###
sort: ( ) ->
byDateDesc = (a,b) ->
if a.safeStartDate.isBefore(b.safeStartDate)
then 1
else ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0
@work && @work.sort byDateDesc
@education && @education.sort byDateDesc
@volunteer && @volunteer.sort byDateDesc
@awards && @awards.sort (a, b) ->
if a.safeDate.isBefore b.safeDate
then 1
else (a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
@publications && @publications.sort (a, b) ->
if ( a.safeReleaseDate.isBefore(b.safeReleaseDate) )
then 1
else ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0
dupe: () ->
rnew = new JRSResume()
rnew.parse this.stringify(), { }
rnew
###*
Create a copy of this resume in which all fields have been interpreted as
Markdown.
###
harden: () ->
that = @
ret = @dupe()
HD = (txt) -> '@@@@~' + txt + '~@@@@'
HDIN = (txt) ->
#return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
return HD txt
# TODO: refactor recursion
hardenStringsInObject = ( obj, inline ) ->
return if !obj
inline = inline == undefined || inline
if Object.prototype.toString.call( obj ) == '[object Array]'
obj.forEach (elem, idx, ar) ->
if typeof elem == 'string' || elem instanceof String
ar[idx] = if inline then HDIN(elem) else HD( elem )
else
hardenStringsInObject elem
else if typeof obj == 'object'
Object.keys( obj ).forEach (key) ->
sub = obj[key]
if typeof sub == 'string' || sub instanceof String
if _.contains(['skills','url','website','startDate','endDate',
'releaseDate','date','phone','email','address','postalCode',
'city','country','region'], key)
return
if key == 'summary'
obj[key] = HD( obj[key] )
else
obj[key] = if inline then HDIN( obj[key] ) else HD( obj[key] )
else
hardenStringsInObject sub
Object.keys( ret ).forEach (member) ->
hardenStringsInObject ret[ member ]
ret
###* Get the default (empty) sheet. ###
JRSResume.default = () ->
new JRSResume().parseJSON require('fresh-resume-starter').jrs
###*
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
###
JRSResume.stringify = ( obj ) ->
replacer = ( key,value ) -> # Exclude these keys from stringification
temp = _.some ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview', 'display_progress_bar'],
( val ) -> return key.trim() == val
return if temp then undefined else value
JSON.stringify obj, replacer, 2
###*
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
###
_parseDates = () ->
_fmt = require('./fluent-date').fmt
@work && @work.forEach (job) ->
job.safeStartDate = _fmt( job.startDate )
job.safeEndDate = _fmt( job.endDate )
@education && @education.forEach (edu) ->
edu.safeStartDate = _fmt( edu.startDate )
edu.safeEndDate = _fmt( edu.endDate )
@volunteer && @volunteer.forEach (vol) ->
vol.safeStartDate = _fmt( vol.startDate )
vol.safeEndDate = _fmt( vol.endDate )
@awards && @awards.forEach (awd) ->
awd.safeDate = _fmt( awd.date )
@publications && @publications.forEach (pub) ->
pub.safeReleaseDate = _fmt( pub.releaseDate )
###*
Export the JRSResume function/ctor.
###
module.exports = JRSResume

88
src/core/jrs-theme.coffee Normal file
View File

@ -0,0 +1,88 @@
###*
Definition of the JRSTheme class.
@module core/jrs-theme
@license MIT. See LICENSE.MD for details.
###
_ = require 'underscore'
PATH = require 'path'
parsePath = require 'parse-filepath'
pathExists = require('path-exists').sync
###*
The JRSTheme class is a representation of a JSON Resume theme asset.
@class JRSTheme
###
class JRSTheme
###*
Open and parse the specified theme.
@method open
###
open: ( thFolder ) ->
@folder = thFolder
# Open the [theme-name].json file; should have the same
# name as folder
pathInfo = parsePath thFolder
# Open and parse the theme's package.json file.
pkgJsonPath = PATH.join thFolder, 'package.json'
if pathExists pkgJsonPath
thApi = require thFolder
thPkg = require pkgJsonPath
this.name = thPkg.name
this.render = (thApi && thApi.render) || undefined
this.engine = 'jrs'
# Create theme formats (HTML and PDF). Just add the bare minimum mix of
# properties necessary to allow JSON Resume themes to share a rendering
# path with FRESH themes.
this.formats =
html:
outFormat: 'html'
files: [{
action: 'transform',
render: this.render,
primary: true,
ext: 'html',
css: null
}]
pdf:
outFormat: 'pdf'
files: [{
action: 'transform',
render: this.render,
primary: true,
ext: 'pdf',
css: null
}]
else
throw { fluenterror: HACKMYSTATUS.missingPackageJSON };
@
###*
Determine if the theme supports the output format.
@method hasFormat
###
hasFormat: ( fmt ) -> _.has this.formats, fmt
###*
Return the requested output format.
@method getFormat
###
getFormat: ( fmt ) -> @formats[ fmt ]
module.exports = JRSTheme;

View File

@ -0,0 +1,109 @@
###*
Definition of the ResumeFactory class.
@license MIT. See LICENSE.md for details.
@module core/resume-factory
###
FS = require('fs')
HACKMYSTATUS = require('./status-codes')
HME = require('./event-codes')
ResumeConverter = require('fresh-jrs-converter')
chalk = require('chalk')
SyntaxErrorEx = require('../utils/syntax-error-ex')
_ = require('underscore')
require('string.prototype.startswith')
###*
A simple factory class for FRESH and JSON Resumes.
@class ResumeFactory
###
ResumeFactory = module.exports =
###*
Load one or more resumes from disk.
@param {Object} opts An options object with settings for the factory as well
as passthrough settings for FRESHResume or JRSResume. Structure:
{
format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null)
objectify: true, // FRESH/JRSResume or raw JSON?
inner: { // Passthru options for FRESH/JRSResume
sort: false
}
}
###
load: ( sources, opts, emitter ) ->
sources.map( (src) ->
@loadOne( src, opts, emitter )
, @)
###* Load a single resume from disk. ###
loadOne: ( src, opts, emitter ) ->
toFormat = opts.format # Can be null
objectify = opts.objectify
# Get the destination format. Can be 'fresh', 'jrs', or null/undefined.
toFormat && (toFormat = toFormat.toLowerCase().trim())
# Load and parse the resume JSON
info = _parse src, opts, emitter
return info if info.fluenterror
# Determine the resume format: FRESH or JRS
json = info.json
isFRESH = json.meta && json.meta.format && json.meta.format.startsWith('FRESH@');
orgFormat = if isFRESH then 'fresh' else 'jrs'
# Convert between formats if necessary
if toFormat and ( orgFormat != toFormat )
json = ResumeConverter[ 'to' + toFormat.toUpperCase() ]( json )
# Objectify the resume, that is, convert it from JSON to a FRESHResume
# or JRSResume object.
rez = null
if objectify
ResumeClass = require('../core/' + (toFormat || orgFormat) + '-resume');
rez = new ResumeClass().parseJSON( json, opts.inner );
rez.i().file = src;
file: src
json: info.json
rez: rez
_parse = ( fileName, opts, eve ) ->
rawData = null
try
# Read the file
eve && eve.stat( HME.beforeRead, { file: fileName });
rawData = FS.readFileSync( fileName, 'utf8' );
eve && eve.stat( HME.afterRead, { file: fileName, data: rawData });
# Parse the file
eve && eve.stat HME.beforeParse, { data: rawData }
ret = { json: JSON.parse( rawData ) }
orgFormat =
if ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@')
then 'fresh' else 'jrs'
eve && eve.stat HME.afterParse, { file: fileName, data: ret.json, fmt: orgFormat }
return ret
catch
# Can be ENOENT, EACCES, SyntaxError, etc.
fluenterror: if rawData then HACKMYSTATUS.parseError else HACKMYSTATUS.readError
inner: _error
raw: rawData
file: fileName

380
src/core/resume.json Normal file
View File

@ -0,0 +1,380 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Resume Schema",
"type": "object",
"additionalProperties": false,
"properties": {
"basics": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string"
},
"label": {
"type": "string",
"description": "e.g. Web Developer"
},
"picture": {
"type": "string",
"description": "URL (as per RFC 3986) to a picture in JPEG or PNG format"
},
"email": {
"type": "string",
"description": "e.g. thomas@gmail.com",
"format": "email"
},
"phone": {
"type": "string",
"description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923"
},
"website": {
"type": "string",
"description": "URL (as per RFC 3986) to your website, e.g. personal homepage",
"format": "uri"
},
"summary": {
"type": "string",
"description": "Write a short 2-3 sentence biography about yourself"
},
"location": {
"type": "object",
"additionalProperties": true,
"properties": {
"address": {
"type": "string",
"description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li."
},
"postalCode": {
"type": "string"
},
"city": {
"type": "string"
},
"countryCode": {
"type": "string",
"description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN"
},
"region": {
"type": "string",
"description": "The general region where you live. Can be a US state, or a province, for instance."
}
}
},
"profiles": {
"type": "array",
"description": "Specify any number of social networks that you participate in",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"network": {
"type": "string",
"description": "e.g. Facebook or Twitter"
},
"username": {
"type": "string",
"description": "e.g. neutralthoughts"
},
"url": {
"type": "string",
"description": "e.g. http://twitter.com/neutralthoughts"
}
}
}
}
}
},
"work": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"company": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"volunteer": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"organization": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"education": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"institution": {
"type": "string",
"description": "e.g. Massachusetts Institute of Technology"
},
"area": {
"type": "string",
"description": "e.g. Arts"
},
"studyType": {
"type": "string",
"description": "e.g. Bachelor"
},
"startDate": {
"type": "string",
"description": "e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"gpa": {
"type": "string",
"description": "grade point average, e.g. 3.67/4.0"
},
"courses": {
"type": "array",
"description": "List notable courses/subjects",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. H1302 - Introduction to American history"
}
}
}
}
},
"awards": {
"type": "array",
"description": "Specify any awards you have received throughout your professional career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"title": {
"type": "string",
"description": "e.g. One of the 100 greatest minds of the century"
},
"date": {
"type": "string",
"description": "e.g. 1989-06-12",
"format": "date"
},
"awarder": {
"type": "string",
"description": "e.g. Time Magazine"
},
"summary": {
"type": "string",
"description": "e.g. Received for my work with Quantum Physics"
}
}
}
},
"publications": {
"type": "array",
"description": "Specify your publications through your career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. The World Wide Web"
},
"publisher": {
"type": "string",
"description": "e.g. IEEE, Computer Magazine"
},
"releaseDate": {
"type": "string",
"description": "e.g. 1990-08-01"
},
"website": {
"type": "string",
"description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html"
},
"summary": {
"type": "string",
"description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML."
}
}
}
},
"skills": {
"type": "array",
"description": "List out your professional skill-set",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Web Development"
},
"level": {
"type": "string",
"description": "e.g. Master"
},
"keywords": {
"type": "array",
"description": "List some keywords pertaining to this skill",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. HTML"
}
}
}
}
},
"languages": {
"type": "array",
"description": "List any other languages you speak",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"language": {
"type": "string",
"description": "e.g. English, Spanish"
},
"fluency": {
"type": "string",
"description": "e.g. Fluent, Beginner"
}
}
}
},
"interests": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Philosophy"
},
"keywords": {
"type": "array",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Friedrich Nietzsche"
}
}
}
}
},
"references": {
"type": "array",
"description": "List references you have received",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Timothy Cook"
},
"reference": {
"type": "string",
"description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing."
}
}
}
}
}
}

View File

@ -0,0 +1,35 @@
###*
Status codes for HackMyResume.
@module core/status-codes
@license MIT. See LICENSE.MD for details.
###
module.exports =
success: 0
themeNotFound: 1
copyCss: 2
resumeNotFound: 3
missingCommand: 4
invalidCommand: 5
resumeNotFoundAlt: 6
inputOutputParity: 7
createNameMissing: 8
pdfGeneration: 9
missingPackageJSON: 10
invalid: 11
invalidFormat: 12
notOnPath: 13
readError: 14
parseError: 15
fileSaveError: 16
generateError: 17
invalidHelperUse: 18
mixedMerge: 19
invokeTemplate: 20
compileTemplate: 21
themeLoad: 22
invalidParamCount: 23
missingParam: 24
createError: 25
validateError: 26

View File

@ -0,0 +1,22 @@
###*
Definition of the BaseGenerator class.
@module generators/base-generator
@license MIT. See LICENSE.md for details.
###
###*
The BaseGenerator class is the root of the generator hierarchy. Functionality
common to ALL generators lives here.
###
module.exports = class BaseGenerator
###* Base-class initialize. ###
constructor: ( @format ) ->
###* Status codes. ###
codes: require '../core/status-codes'
###* Generator options. ###
opts: { }

View File

@ -0,0 +1,30 @@
###*
Definition of the HTMLGenerator class.
@module generators/html-generator
@license MIT. See LICENSE.md for details.
###
TemplateGenerator = require './template-generator'
FS = require 'fs-extra'
HTML = require 'html'
PATH = require 'path'
require 'string.prototype.endswith'
module.exports = class HtmlGenerator extends TemplateGenerator
constructor: -> super 'html'
###*
Copy satellite CSS files to the destination and optionally pretty-print
the HTML resume prior to saving.
###
onBeforeSave: ( info ) ->
if info.outputFile.endsWith '.css'
return info.mk
if @opts.prettify
then HTML.prettyPrint info.mk, this.opts.prettify
else info.mk

View File

@ -0,0 +1,92 @@
###*
Definition of the HtmlPdfCLIGenerator class.
@module generators/html-pdf-generator.js
@license MIT. See LICENSE.md for details.
###
TemplateGenerator = require './template-generator'
FS = require 'fs-extra'
PATH = require 'path'
SLASH = require 'slash'
_ = require 'underscore'
HMSTATUS = require '../core/status-codes'
SPAWN = require '../utils/safe-spawn'
###*
An HTML-driven PDF resume generator for HackMyResume. Talks to Phantom,
wkhtmltopdf, and other PDF engines over a CLI (command-line interface).
If an engine isn't installed for a particular platform, error out gracefully.
###
module.exports = class HtmlPdfCLIGenerator extends TemplateGenerator
constructor: () -> super 'pdf', 'html'
###* Generate the binary PDF. ###
onBeforeSave: ( info ) ->
#console.dir _.omit( info, 'mk' ), depth: null, colors: true
return info.mk if info.ext != 'html' and info.ext != 'pdf'
safe_eng = info.opts.pdf || 'wkhtmltopdf'
safe_eng = 'phantomjs' if safe_eng == 'phantom'
if _.has engines, safe_eng
@errHandler = info.opts.errHandler
engines[ safe_eng ].call @, info.mk, info.outputFile, @onError
return null # halt further processing
### Low-level error callback for spawn(). May be called after HMR process
termination, so object references may not be valid here. That's okay; if
the references are invalid, the error was already logged. We could use
spawn-watch here but that causes issues on legacy Node.js. ###
onError: (ex, param) ->
param.errHandler?.err? HMSTATUS.pdfGeneration, ex
return
# TODO: Move each engine to a separate module
engines =
###*
Generate a PDF from HTML using wkhtmltopdf's CLI interface.
Spawns a child process with `wkhtmltopdf <source> <target>`. wkhtmltopdf
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease wkhtmltopdf rendering
###
wkhtmltopdf: (markup, fOut, on_error) ->
# Save the markup to a temporary file
tempFile = fOut.replace /\.pdf$/i, '.pdf.html'
FS.writeFileSync tempFile, markup, 'utf8'
SPAWN 'wkhtmltopdf', [ tempFile, fOut ], false, on_error, @
return
###*
Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
###
phantomjs: ( markup, fOut, on_error ) ->
# Save the markup to a temporary file
tempFile = fOut.replace /\.pdf$/i, '.pdf.html'
FS.writeFileSync tempFile, markup, 'utf8'
scriptPath = PATH.relative process.cwd(), PATH.resolve( __dirname, '../utils/rasterize.js' )
scriptPath = SLASH scriptPath
sourcePath = SLASH PATH.relative( process.cwd(), tempFile)
destPath = SLASH PATH.relative( process.cwd(), fOut)
SPAWN 'phantomjs', [ scriptPath, sourcePath, destPath ], false, on_error, @
return

View File

@ -0,0 +1,52 @@
###*
Definition of the HtmlPngGenerator class.
@module generators/html-png-generator
@license MIT. See LICENSE.MD for details.
###
TemplateGenerator = require './template-generator'
FS = require 'fs-extra'
HTML = require 'html'
SLASH = require 'slash'
SPAWN = require '../utils/safe-spawn'
PATH = require 'path'
###*
An HTML-based PNG resume generator for HackMyResume.
###
module.exports = class HtmlPngGenerator extends TemplateGenerator
constructor: -> super 'png', 'html'
invoke: ( rez, themeMarkup, cssInfo, opts ) ->
# TODO: Not currently called or callable.
generate: ( rez, f, opts ) ->
htmlResults = opts.targets.filter (t) -> t.fmt.outFormat == 'html'
htmlFile = htmlResults[0].final.files.filter (fl) ->
fl.info.ext == 'html'
phantom htmlFile[0].data, f
return
###*
Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
###
phantom = ( markup, fOut ) ->
# Save the markup to a temporary file
tempFile = fOut.replace(/\.png$/i, '.png.html');
FS.writeFileSync tempFile, markup, 'utf8'
scriptPath = SLASH( PATH.relative( process.cwd(),
PATH.resolve( __dirname, '../utils/rasterize.js' ) ) );
sourcePath = SLASH( PATH.relative( process.cwd(), tempFile) );
destPath = SLASH( PATH.relative( process.cwd(), fOut) );
info = SPAWN('phantomjs', [ scriptPath, sourcePath, destPath ]);
return

View File

@ -0,0 +1,25 @@
###*
Definition of the JsonGenerator class.
@module generators/json-generator
@license MIT. See LICENSE.md for details.
###
BaseGenerator = require './base-generator'
FS = require 'fs'
_ = require 'underscore'
FJCV = require 'fresh-jrs-converter'
###* The JsonGenerator generates a FRESH or JRS resume as an output. ###
module.exports = class JsonGenerator extends BaseGenerator
constructor: () -> super 'json'
invoke: ( rez ) ->
altRez = FJCV[ 'to' + if rez.format() == 'FRESH' then 'JRS' else 'FRESH' ] rez
altRez = FJCV.toSTRING( altRez )
#altRez.stringify()
generate: ( rez, f ) ->
FS.writeFileSync f, @invoke(rez), 'utf8'
return

View File

@ -0,0 +1,31 @@
###*
Definition of the JsonYamlGenerator class.
@module generators/json-yaml-generator
@license MIT. See LICENSE.md for details.
###
BaseGenerator = require('./base-generator')
FS = require('fs')
YAML = require('yamljs')
###*
JsonYamlGenerator takes a JSON resume object and translates it directly to
JSON without a template, producing an equivalent YAML-formatted resume. See
also YamlGenerator (yaml-generator.js).
###
module.exports = class JsonYamlGenerator extends BaseGenerator
constructor: () -> super 'yml'
invoke: ( rez, themeMarkup, cssInfo, opts ) ->
YAML.stringify JSON.parse( rez.stringify() ), Infinity, 2
generate: ( rez, f, opts ) ->
data = YAML.stringify JSON.parse( rez.stringify() ), Infinity, 2
FS.writeFileSync f, data, 'utf8'
data

Some files were not shown because too many files have changed in this diff Show More