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

Compare commits

...

162 Commits

Author SHA1 Message Date
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
58b6ad841e Remove unused "main" entry from package.json.
Clients who were require()ing hackmyresume should now require()
hackmycore instead.
2016-01-24 17:15:32 -05:00
fc937e3ec8 Update "hackmyapi" references. 2016-01-24 17:14:53 -05:00
8652c7ecdf Rename & bump hackmyapi dependency. 2016-01-24 17:04:01 -05:00
c882235eff Bump version to 1.7.0. 2016-01-24 17:03:35 -05:00
d6c5239f9e Update roadmap.
Use linkable section headings for easier referencing.
2016-01-24 16:56:38 -05:00
4b2db3f720 Introduce dev roadmap. 2016-01-24 16:11:56 -05:00
9736777828 Fix Travis. 2016-01-24 10:53:32 -05:00
d3194fba19 Relocate internal sources to HackMyAPI.
Move internal sources and related tests to:

https://github.com/hacksalot/HackMyAPI
2016-01-24 09:55:04 -05:00
fa29f9794d Update README image. 2016-01-24 06:23:15 -05:00
07915002bb Adjust "merging X onto Y" output. 2016-01-24 05:35:07 -05:00
fbcc06dcda Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-24 05:17:24 -05:00
7413a3a257 Fixed type 2016-01-24 05:17:02 -05:00
e6d2255291 Scrub. 2016-01-23 23:30:48 -05:00
2840ec3f87 Introduce {{fontSize}} helper. 2016-01-23 22:40:33 -05:00
05cd863ebf Add PDF engines to man page. 2016-01-23 20:30:23 -05:00
20961afb62 Introduce {{color}} helper. 2016-01-23 20:24:35 -05:00
1256095e25 Support "fonts.all" in FRESH themes.
Add support for default font specs in FRESH theme.json files. The "all"
format matches any format that doesn't have a specific key in "fonts".
2016-01-23 03:58:11 -05:00
f073c79b8d Better dynamic font handling. 2016-01-22 22:19:28 -05:00
ac9e4aa1a0 Capture CHANGELOG and FAQ. 2016-01-22 20:00:07 -05:00
915f35b1e6 Improve Underscore.js rendering support. 2016-01-22 10:36:26 -05:00
4fe74057f9 Improve font helpers.
Log a warning on incorrect use.
2016-01-22 08:33:01 -05:00
5a1ec033bb Adjust USE.txt.
--opts has changed to --options and --no-tips to --tips.
2016-01-22 08:27:21 -05:00
6801e39f97 Tweak output colorization. 2016-01-22 04:55:29 -05:00
f6f383751f Fix JSON Resume theme rendering glitch. 2016-01-22 03:05:41 -05:00
43ed564a6e Disable tips and theme messages by default.
Instead of displaying tips by default and allowing users to turn them
off with --no-tips, hide tips by default and allow users to show them
with --tips.
2016-01-22 02:51:00 -05:00
7b3364c356 Document parameter. 2016-01-22 02:44:17 -05:00
58a7fc09e5 Add toUpper helper. 2016-01-22 02:44:04 -05:00
01c053702d Gather. 2016-01-21 23:40:15 -05:00
a935fe7dc2 Introduce {{fontFace}} helper. 2016-01-21 23:39:30 -05:00
c9825fa016 Update .gitignore. 2016-01-21 23:23:24 -05:00
9eb9207348 Mention string.prototype.endswith dependency. 2016-01-21 05:41:39 -05:00
6b171e69db Improve CSS handling. 2016-01-21 05:21:49 -05:00
5b0ee89e34 Bump FRESCA to 0.6.0. 2016-01-20 21:44:01 -05:00
8bd3ddc7fd Bump fresh-resume-starter to 0.2.2. 2016-01-20 21:43:49 -05:00
984ae95576 Cleanup. 2016-01-20 21:43:11 -05:00
f77cced7f3 Improve error handling. 2016-01-20 19:59:36 -05:00
57787f1bc7 Re-introduce API-level tests. 2016-01-20 01:48:57 -05:00
9419f905df Build verb invocation should return JSON result. 2016-01-20 01:48:33 -05:00
001fd893c1 Add tests for partial resume loading (FRESH). 2016-01-20 00:21:07 -05:00
babe4b4b31 Bump versions. 2016-01-20 00:20:10 -05:00
201f39442e Add support for .ignore flag in FRESH and JRS resumes.
Preliminary support for ".ignore" on any non-leaf FRESH or JRS node.
Nodes (employment entries, education stints, etc.) decorated with
".ignore" will be treated by HMR as if they weren't present.
2016-01-19 20:09:59 -05:00
47f6aff561 Improve keyword regex.
Better support for simple keywords like "C" or "R".
2016-01-19 19:10:20 -05:00
cef9a92cb6 Introduce CHANGELOG.md. 2016-01-19 16:42:48 -05:00
2253e4ead7 Fix theme counts.
The N in "Applying theme FOOBAR (N formats)" should reflect the count of
explicit + freebie output formats.
2016-01-19 16:01:34 -05:00
2f628f8564 Reconnect process exit codes. 2016-01-18 20:06:45 -05:00
23cd52885b Swallow inline failures in CONVERT. 2016-01-18 19:21:25 -05:00
181419ae28 Improve PEEK command behavior. 2016-01-18 19:20:17 -05:00
a81ad0fef2 Tweak build command error condition. 2016-01-18 18:36:24 -05:00
d220cedfeb Improve behavior of PEEK command. 2016-01-18 18:35:38 -05:00
e72564162b Remove custom "extend" method.
Replace with NPM extend.
2016-01-18 17:31:08 -05:00
c98d05270e Improve error handling. 2016-01-18 17:13:37 -05:00
3e3803ed85 Improve error handling. 2016-01-18 14:10:35 -05:00
c8d8e566f8 Add IIFE. 2016-01-18 14:10:25 -05:00
712cba57b8 Capture. 2016-01-18 00:34:57 -05:00
c9e45d4991 Capture. 2016-01-17 21:46:58 -05:00
e9edc0d15c Bump fresh-test-resumes to 0.5.0. 2016-01-16 16:34:25 -05:00
b99a09c88a Integrate tests with new fresh-jrs-converter structure. 2016-01-16 16:29:00 -05:00
5c95fe7af1 Integrate with fresh-jrs-converter.
Move FRESH/JRS conversion logic (and all future format conversions) into
a separate repo.
2016-01-16 12:40:16 -05:00
17f2ebb753 Modularize messages.
...and move strings out of error.js.
2016-01-15 23:46:43 -05:00
fc67f680ee Move output messages to YAML. 2016-01-15 22:52:10 -05:00
88879257e6 Document PEEK command.
Add preliminary docs around PEEK.
2016-01-15 14:46:13 -05:00
fff45e1431 Update tests. 2016-01-15 13:36:57 -05:00
934d8a6123 Update --options file loading. 2016-01-15 13:36:20 -05:00
defe9b6e95 Remove magic number. 2016-01-15 13:35:45 -05:00
4c5ccc001a Introduce PEEK command.
Peek at arbitrary resumes and resume objects paths with "hackmyresume
peek <resume> [objectPath]". For ex:

hackmyresume PEEK resume.json
hackmyresume PEEK resume.json info
hackmyresume PEEK resume.json employment[2].keywords
hackmyresume PEEK r1.json r2.json r3.json info.brief
2016-01-15 13:08:01 -05:00
de5c2ecb95 Update dependencies. 2016-01-14 23:39:54 -05:00
dbb95aef3a Bump fresh-resume-starter / fresh-test-resumes. 2016-01-14 15:53:03 -05:00
c9ae2ffef3 Improve errors / tests consistency. 2016-01-14 14:22:26 -05:00
86af2a2c4f Rename test-cli.js to test-api.js. 2016-01-14 12:07:43 -05:00
37ea6cf804 Rename error-handler.js to error.js. 2016-01-14 11:49:27 -05:00
a9c685c6a4 Refactor error handling (interim). 2016-01-14 11:47:05 -05:00
7765e85336 Integrate printf(). 2016-01-14 09:46:29 -05:00
7af50c51f6 Gather. 2016-01-14 08:48:07 -05:00
19b30d55ec Move error handling out of core. 2016-01-13 15:28:02 -05:00
eddda8146e Bump FRESCA and fresh-themes versions. 2016-01-13 14:46:35 -05:00
1a0b91a58f Update conversion tests. 2016-01-12 18:14:06 -05:00
1b94ada709 Misc improvements. 2016-01-12 18:13:54 -05:00
1966b0a862 Move string transformation out of FRESHResume. 2016-01-12 13:28:20 -05:00
8ced6a730a Fix BUILD command event notifications. 2016-01-12 12:46:55 -05:00
6cd1e60e79 Sort projects. 2016-01-12 12:46:18 -05:00
be691e4230 Remove commented lines. 2016-01-12 12:46:05 -05:00
07b23109f9 Use async spawn() by default. 2016-01-12 12:32:32 -05:00
32769a2b0b Update license to 2016. 2016-01-11 21:16:11 -05:00
280977cb62 Update package.json contributors. 2016-01-11 21:16:01 -05:00
ddceec68a2 Improve --options tests. 2016-01-11 21:15:28 -05:00
b961fd1c07 Fix global leak. 2016-01-11 21:14:40 -05:00
342b960f63 Add tests for raw JSON and file via --options / -o. 2016-01-11 20:52:17 -05:00
f965bf456a Fix JSON file loading glitch with --options. 2016-01-11 20:52:07 -05:00
69be38110f Update license notice in index.js. 2016-01-11 19:56:44 -05:00
3800e19418 Process TXT global partials. 2016-01-11 19:56:19 -05:00
e29ed58a1c Tests: Update theme name. 2016-01-11 18:08:31 -05:00
11bfcd4bef Support raw JSON in the --options parameter. 2016-01-11 18:07:56 -05:00
fbc2e9a4db Bump version to 1.6.0. 2016-01-11 14:04:05 -05:00
7814786957 Recruit Markdown partials when present. 2016-01-11 12:36:00 -05:00
542776fd2e Add shortcut options to man page. 2016-01-11 08:31:05 -05:00
815ee3dc7e Support lowercase -v version flag.
Commander.js built-in version handling uses an uppercase shortcut (-V)
for the version, so the common -v (lowercase) isn't recognized and
errors out.
2016-01-11 08:29:46 -05:00
376e720f4b Scrub. 2016-01-11 08:21:06 -05:00
b224c8939b Remove redundant conditional. 2016-01-11 08:20:48 -05:00
0ecac98cff Remove totally unnecessary line.
Totally.
2016-01-10 19:11:43 -05:00
1416f57d0b Move verb.js to /verbs folder. 2016-01-10 19:08:29 -05:00
65c7e41c53 Remove unused var. 2016-01-10 19:02:24 -05:00
c8cc673ad5 Update man page. 2016-01-10 18:48:57 -05:00
656dbe2fc2 Capture. 2016-01-10 14:53:22 -05:00
a4ee7127ee Fix stack reporting glitch. 2016-01-10 13:28:20 -05:00
fee21a7b17 Always use JSONLint for SyntaxError post-processing.
Remove the check for SyntaxError's built-in line and character
indicators and always re-parse on error to grab the line/column.
2016-01-10 05:17:28 -05:00
32fd8dc636 Merge pull request #102 from beeryt/master
Fixed typo
2016-01-10 02:27:03 -05:00
2c8f444d42 Fixed type 2016-01-09 21:12:19 -08:00
bd8b587c5b Remove explicit logger and error handler params. 2016-01-09 22:34:21 -05:00
4c954b79df Scrub. 2016-01-09 22:15:50 -05:00
b7fffbcf73 Update helper reference in analysis .hbs. 2016-01-09 22:14:34 -05:00
0829800b65 Move helpers to /helpers. 2016-01-09 22:13:29 -05:00
d7cfc76636 Promote console helpers has to console-helpers.js. 2016-01-09 22:11:06 -05:00
311030474d Tests: Remove hard-coded version number. 2016-01-09 20:29:30 -05:00
ec69e668ff Bump version to 1.5.3. 2016-01-09 20:21:17 -05:00
f18910f490 Generate ANALYZE console output from Handlebars template. 2016-01-09 20:18:56 -05:00
540ad48d61 Scrub. 2016-01-09 16:56:30 -05:00
540c745069 Exclude Emacs cruft. 2016-01-09 16:44:00 -05:00
c5b8eec33a Move CLI-related assets to subfolder. 2016-01-09 16:14:28 -05:00
bece335a64 Fix CREATE verb output. 2016-01-09 15:58:39 -05:00
3aabb5028d Continue moving logging out of core. 2016-01-09 15:49:08 -05:00
732bc9809a Start moving logging out of core. 2016-01-09 13:58:47 -05:00
d77b484e55 Verbs are event emitters.
Let verbs source events through EventEmitter. Using aggregation is a bit
simpler here than extending because of the Resig "Class" stuff.
2016-01-09 08:12:55 -05:00
43564bf380 Update tests. 2016-01-09 06:44:47 -05:00
88c71f6e9c Move commands to Verb hierarchy
Move flat command functions (BUILD, ANALYZE, etc.) to a shallow Verb
hierarchy. Allow command verbs to inherit common functionality and prep
for better debugging/logging as well as test mocks.
2016-01-09 06:44:22 -05:00
47e8605f50 Handle args in mock/passthrough case. 2016-01-09 05:30:12 -05:00
9466a8c0dd Remove spawn-watch.
No longer necessary.
2016-01-09 05:29:45 -05:00
d878270bc6 Encapsulate CLI interface to ease testing.
Strip index.js down to its bare essentials, move primary logic to
main.js, and expose the latter via module.exports. This allows tests to
execute the same code path(s) HMR runs in production.
2016-01-08 19:22:44 -05:00
189 changed files with 12783 additions and 5338 deletions

36
.gitignore vendored
View File

@ -1,4 +1,40 @@
node_modules/
tests/sandbox/
doc/
docs/
local/
npm-debug.log
# Emacs detritus
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/

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

306
CHANGELOG.md Normal file
View File

@ -0,0 +1,306 @@
CHANGELOG
=========
## v1.6.0
### Major Improvements
- Better consistency and coverage for all FRESH resumes and themes ([#45][i45]).
- Initial support for overridable fonts in FRESH themes. Like a particular
theme, but want to change the typography? The specific fonts used by a theme
can now be overridden by the user. (FRESH themes only).
- New resume sections! Support for `projects` and `affiliation` resume sections
for technical and creative projects and memberships / clubs / associations,
respectively ([#92][i92]).
- New command! `PEEK` at any arbitrary field or entry on your `.json` resume.
### Added
- Improved handling of start and end dates on `employment`, `projects`,
`education`, and other sections with start/end dates.
- Support for an `.ignore` property on any FRESH or JSON Resume section or field.
Ignored properties will be treated by HackMyResume as if they weren't present.
- Emit extended status and error info with the `--debug` or `-d` switch.
- The `-o` or `--options` switch can now handle either the path to a **JSON
settings file** or **raw JSON/JavaScript**. Since the JSON double quote syntax
is a bit cumbersome from the command line, HackMyResume accepts regular
JavaScript object literal syntax:
hackmyresume build resume.json -o "{ theme: 'compact', silent: 'true' }"
- Ability to disable sorting of resume sections (employments, projects, etc.)
with the `--no-sort` option. HMR will respect the order of items as they appear
in your resume `.json` file.
- Improvements to the starter resume emitted by `hackmyresume new`.
- Theme Authoring: Annotated the HTML and MS Word (XML) formats of the Modern
theme for FRESH theme authors.
- Theme Authoring: Support for templatized CSS files in FRESH themes. CSS files
are now expanded via Handlebars or Underscore prior to copying to the
destination.
- Added CHANGELOG.md (this file).
### Changed
- Rewrote the HackMyResume man/help page.
- Minor incremental updates to the [FRESCA][fresca] schema.
- PDF generation now uses asynchronous `spawn()` which has better compatibility
with old or boutique versions of Node.js.
- Refactored colors in HackMyResume output. Errors will now display as red,
warnings as yellow, successful operations as green, and informational messages
as cyan.
- Theme messages and usage tips will no longer display during resume generation
by default. Use the `--tips` option to view them.
- The `--no-tips` option (default: false) has been replaced with the `--tips`
option, also defaulting to false.
- Removed the `hello-world` theme from the [prebuilt themes][themes] that ship
with HackMyResume. It can be installed separately from NPM:
```bash
npm install fresh-theme-hello-world
hackmyresume resume.json -t node_modules/fresh-theme-hello-world
```
- sd
### Fixed
- PDF generation issues on older versions of Node.
- Stack traces not being emitted correctly.
- Missing `speaking` section will now appear on generated resumes ([#101][i101]).
- Incomplete `education` details will now appear on generated resumes ([#65][i65]).
- Missing employment end date being interpreted as "employment ends today"
([#84][i84]).
- Merging multiple source resumes during `BUILD` sometimes fails.
- Document `--pdf` flag in README ([#111][i111]).
### Internal
- Logging messages have been moved out of core HackMyResume code ahead of
localization support.
- All HackMyResume console output is described in `msg.yml`.
- Relaxed pure JavaScript requirement. CoffeeScript will now start appearing
in HackMyResume and FluentCV sources!
- Additional tests.
## v1.5.2
### Fixed
- Tweak stack trace under `--debug`.
## v1.5.1
### Added
- Preliminary support for `-d` or `--debug` flag. Forces HackMyResume to emit a stack trace under error conditions.
## v1.5.0
### Added
- HackMyResume now supports **CLI-based generation of PDF formats across multiple engines (Phantom, wkhtmltopdf, etc)**. Instead of talking to these engines over a programmatic API, as in prior versions, HackMyResume 1.5+ speaks to them over the same command-line interface (CLI) you'd use if you were using these tools directly.
- HackMyResume will now (attempt to) **generate a PDF output for JSON Resume themes** (in addition to HTML).
- Minor README and FAQ additions.
### Changed
- **Cleaner, quicker installs**. Installing HackMyResume with `npm install hackmyresume -g` will no longer trigger a lengthy, potentially error-prone install for Phantom.js and/or wkhtmltopdf for PDF support. Instead, users can install these engines externally and HMR will use them when present.
- Minor error handling improvements.
### Fixed
- Fixed an error with generating specific formats with the `BUILD` command (#97).
- Fixed numerous latent/undocumented bugs and glitches.
## v1.4.2
### Added
- Introduced [FAQ](https://github.com/hacksalot/HackMyResume/blob/master/FAQ.md).
- Additional README notes.
## v1.4.1
### Added
- `hackmyresume new` now generates a [valid starter resume with sample data](https://github.com/fluentdesk/fresh-resume-starter).
### Fixed
- Fixed warning message when `hackmyresume new` is run without a filename.
## v1.4.0
### Added
- **"Projects" support**: FRESH resumes and themes can now store and display
open source, commercial, private, personal, and creative projects.
- **New command: ANALYZE**. Inspect your resume for gaps, keyword counts, and other metrics. (Experimental.)
- **Side-by-side PDF generation** with Phantom and wkhtmltopdf. Use the `--pdf` or `-p` flag to pick between `phantom` and `wkhtmltopdf` generation.
- **Disable PDF generation** with the `--pdf none` switch.
- **Inherit formats between themes**. Themes can now inherit formats (Word, HTML, .txt, etc.) from other themes. (FRESH themes only.)
- **Rename resume sections** to different languages or wordings.
- **Specify complex options via external file**. Use with the `-o` or `--opts` option.
- **Disable colors** with the `--no-color` flag.
- **Theme messages and usage tips** instructions will now appear in the default HackMyResume output for the `build` command. Run `hackmyresume build resume.json -t awesome` for an example. Turn off with the `--no-tips` flag.
- **Treat validation errors as warnings** with the `--assert` switch (VALIDATE command only).
### Fixed
- Fixed a minor glitch in the FRESCA schema.
- Fixed encoding issues in the `Highlights` section of certain resumes.
- Fix behavior of `-s` and `--silent` flags.
### Changed
- PDF generation now defaults to Phantom for all platforms, with `wkhtmltopdf`
accessible with `--pdf wkhtmltopdf`.
- Resumes are now validated, by default, prior to generation. This
behavior can be disabled with the `--novalidate` or `--force` switch.
- Syntax errors in source FRESH and JSON Resumes are now captured for all
commands.
- Minor updates to README.
- Most themes now inherit Markdown and Plain Text formats from the **Basis**
theme.
### Internal
- Switched from color to chalk.
- Command invocations now handled through commander.js.
- Improved FRESH theme infrastructure (more partials, more DRY).
## v1.3.1
### Added
- Add additional Travis badges.
### Fixed
- Fix extraneous console log output when generating a FRESH theme to MS Word.
- Fix Travis tests on `dev`.
## v1.3.0
### Added
- **Local generation of JSON Resume themes**. To use a JSON Resume theme, first install it with `npm install jsonresume-theme-[blah]` (most JSON Resume themes are on NPM). Then pass it into HackMyResume via the `-t` parameter:
`hackmyresume BUILD resume.json TO out/somefile.all -t node_modules/jsonresume-theme-classy`
- **Better Markdown support.** HackMyResume will start flowing basic Markdown styles to JSON Resume (HTML) themes. FRESH's existing Markdown support has also been improved.
- **.PNG output formats** will start appearing in themes that declare an HTML output.
- **Tweak CSS embedding / linking via the --css option** (`<style></style>` vs `<link>`). Only works for HTML (or HTML-driven) formats of FRESH themes. Use `--css=link` to link in CSS assets and `--css=embed` to embed the styles in the HTML document. For example `hackmyresume BUILD resume.json TO out/resume.all --css=link`.
- **Improved Handlebars/Underscore helper support** for FRESH themes. Handlebars themes can access helpers via `{{helperName}}`. Underscore themes can access helpers via the `h` object.
### Changed
- **Distinguish between validation errors and syntax errors** when validating a FRESH or JRS resume with `hackmyresume validate <blah>`.
- **Emit line and column info** for syntax errors during validation of FRESH and JRS resumes.
- **FRESH themes now embed CSS into HTML formats by default** so that the HTML resume output doesn't have an external CSS file dependency by default. Users can specify normal linked stylesheets by setting `--css=link`.
- **Renamed fluent-themes repo to fresh-themes** in keeping with the other parts of the project.
### Fixed
- Fix various encoding errors in MS Word outputs.
- Fix assorted FRESH-to-JRS and JRS-to-FRESH conversion glitches.
- Fix error when running HMR with no parameters.
- Other minor fixes.
## v1.3.0-beta
- Numerous changes supporting v1.3.0.
## v1.2.2
### Fixed
- Various in-passing fixes.
## v1.2.1
### Fixed
- Fix `require('FRESCA')` error.
- Fix `.history` and `.map` errors on loading incomplete or empty JRS resumes.
### Added
- Better test coverage of incomplete/empty resumes.
## v1.2.0
### Fixed
- Fixed the `new` command: Generate a new FRESH-format resume with `hackmyresume new resume.json` or a new JSON Resume with `hackmyresume new resume.json -f jrs`.
### Added
- Introduced CLI tests.
## v1.1.0
### Fixed
- MS Word formats: Fixed skill coloring/level bug.
### Changed
- Make the `TO` keyword optional. If no `TO` keyword is specified (for the `build` and `convert` commands), HMR will assume the last file passed in is the desired output file. So these are equivalent:
```shell
hackmyresume BUILD resume.json TO out/resume.all
hackmyresume BUILD resume.json out/resume.all
```
`TO` only needs to be included if you have multipled output files:
```shell
hackmyresume BUILD resume.json TO out1.doc out2.html out3.tex
```
## v1.0.1
### Fixed
- Correctly generate MS Word hyperlinks from Markdown source data.
## v1.0.0
- Initial public 1.0 release.
[i45]: https://github.com/hacksalot/HackMyResume/issues/45
[i65]: https://github.com/hacksalot/HackMyResume/issues/65
[i84]: https://github.com/hacksalot/HackMyResume/issues/84
[i92]: https://github.com/hacksalot/HackMyResume/issues/92
[i101]: https://github.com/hacksalot/HackMyResume/issues/101
[i111]: https://github.com/hacksalot/HackMyResume/issues/111
[fresca]: https://github.com/fluentdesk/FRESCA
[themes]: https://github.com/fluentdesk/fresh-themes

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

112
FAQ.md
View File

@ -9,19 +9,29 @@ Frequently Asked Questions (FAQ)
3. Test with `hackmyresume BUILD <resume-name>.json`. Look in the `out/` folder.
4. Play around with different themes with the `-t` or `--theme` parameter. You can use any [FRESH](https://github.com/fluentdesk/fresh-themes) or [JSON Resume](https://jsonresume.org/themes) theme. The latter have to be installed first.
4. Play around with different themes with the `-t` or `--theme` parameter.
You can use any [FRESH](https://github.com/fluentdesk/fresh-themes) or
[JSON Resume](https://jsonresume.org/themes) theme. The latter have to be
installed first.
## What is FRESH?
FRESH is the **F**luent **R**esume and **E**mployment **S**ystem for **H**umans. It's an open-source, user-first workflow, schema, and set of practices for technical candidates and recruiters.
FRESH is the **F**luent **R**esume and **E**mployment **S**ystem for **H**umans.
It's an open-source, user-first workflow, schema, and set of practices for
technical candidates and recruiters.
## What is FRESCA?
The **F**RESH **R**esume and **E**mployment **SC**hem**A**&mdash;an open-source, JSON-driven schema for resumes, CVs, and other employment artifacts. FRESCA is the recommended schema/format for FRESH, with optional support for JSON Resume.
The **F**RESH **R**esume and **E**mployment **SC**hem**A**&mdash;an open-source,
JSON-driven schema for resumes, CVs, and other employment artifacts. FRESCA is
the recommended schema/format for FRESH, with optional support for JSON Resume.
## What is JSON Resume?
An [open resume standard](http://jsonresume.org/themes/) sponsored by Hired.com. Like FRESCA, JSON Resume is JSON-driven and open-source. Unlike FRESCA, JSON Resume targets a worldwide audience where FRESCA is optimized for technical candidates.
An [open resume standard](http://jsonresume.org/themes/) sponsored by Hired.com.
Like FRESCA, JSON Resume is JSON-driven and open-source. Unlike FRESCA, JSON
Resume targets a worldwide audience where FRESCA is optimized for technical
candidates.
## Should I use the FRESH or JSON Resume format/schema for my resume?
@ -31,24 +41,45 @@ Both! The workflow we like to use:
2. Convert it to JSON Resume format for additional themes/tools.
3. Maintain both versions.
Both formats are open-source and both formats are JSON-driven. FRESH was designed as a universal container format and superset of existing formats, where the JSON Resume format is intended for a generic audience.
Both formats are open-source and both formats are JSON-driven. FRESH was
designed as a universal container format and superset of existing formats, where
the JSON Resume format is intended for a generic audience.
## How do I use a FRESH theme?
FRESH themes currently come preinstalled with HackMyResume.
Several FRESH themes come preinstalled with HackMyResume; others can be
installed from NPM and GitHub.
1. Specify the theme name in the `--theme` or `-t` parameter to the **build** command:
### To use a preinstalled FRESH theme:
1. Pass the theme name into HackMyResume via the `--theme` or `-t` parameter:
```bash
hackmyresume BUILD my-resume.json --theme <theme-name>
hackmyresume build resume.json --theme compact
```
`<theme-name>` can be one of `positive`, `compact`, `modern`, `minimist`, `hello-world`, or `awesome`.
### To use an external FRESH theme:
2. Check your output folder. Although under FRESH, HTML formats are hardened to a degree for local file access, it's best to view HTML formats over a local web server connection.
1. Install the theme locally. The easiest way to do that is with NPM.
```bash
npm install fresh-theme-underscore
```
2. Pass the theme folder into HackMyResume:
```bash
hackmyresume BUILD resume.json --theme node_modules/fresh-theme-underscore
```
3. Check your output folder. It's best to view HTML formats over a local web
server connection.
## How do I use a JSON Resume theme?
JSON Resume (JRS) themes can be installed from NPM and GitHub and passed into
HackMyResume via the `--theme` or `-t` parameter.
1. Install the theme locally. The easiest way to do that is with NPM.
```bash
@ -61,15 +92,19 @@ FRESH themes currently come preinstalled with HackMyResume.
hackmyresume BUILD resume.json --theme node_modules/jsonresume-theme-classy
```
3. Check your output folder. It's best to view HTML formats over a local web server connection.
3. Check your output folder. It's best to view HTML formats over a local web
server connection.
## Should I keep my resume in version control?
Absolutely! As text-based, JSON-driven documents, both FRESH and JSON Resume are ideal candidates for version control. Future versions of HackMyResume will have this functionality built-in.
Absolutely! As text-based, JSON-driven documents, both FRESH and JSON Resume are
ideal candidates for version control. Future versions of HackMyResume will have
this functionality built in.
## Can I change the default section titles ("Employment", "Skills", etc.)?
If you're using a FRESH theme, yes. First, create a HackMyResume options file mapping resume sections to your preferred section title:
If you're using a FRESH theme, yes. First, create a HackMyResume options file
mapping resume sections to your preferred section title:
```javascript
// myoptions.json
@ -90,6 +125,57 @@ hackmyresume BUILD resume.json -o myoptions.json
This ability is currently only supported for FRESH resume themes.
## How does resume merging work?
Resume merging is a way of storing your resume in separate files that
HackMyResume will merge into a single "master" resume file prior to generating
specific output formats like HTML or PDF. It's a way of producing flexible,
configurable, targeted resumes with minimal duplication.
For example, a software developer who moonlights as a game programmer might
create three FRESH or JRS resumes at different levels of specificity:
- **generic.json**: A generic technical resume, suitable for all audiences.
- **game-developer.json**: Overrides and amendments for game developer
positions.
- **blizzard.json**: Overrides and amendments specific to a hypothetical
position at Blizzard.
If you run `hackmyresume BUILD generic.json TO out/resume.all`, HMR will
generate all available output formats for the `generic.json` as usual. But if
you instead run...
```bash
hackmyresume BUILD generic.json game-developer.json TO out/resume.all
```
...HackMyResume will notice that multiple source resumes were specified and
merge `game-developer.json` onto `generic.json` before generating, yielding a
resume that's more suitable for game-developer-related positions.
You can take this a step further. Let's say you want to do a targeted resume
submission to a game developer position at Blizzard, and `blizzard.json`
contains the edits and revisions you'd like to show up in the targeted resume.
In that case, merge again! Feed all three resumes to HackMyResume, in order
from most generic to most specific, and HMR will merge them all prior to
generating the final output format(s) for your resume.
```bash
# Merge blizzard.json onto game-developer.json onto generic.json, then build
hackmyresume BUILD generic.json game-developer.json blizzard.json TO out/resume.all
```
There's no limit to the number of resumes you can merge this way.
You can also divide your resume into files containing different sections:
- **resume-a.json**: Contains `info`, `employment`, and `summary` sections.
- **resume-b.json**: Contains all other sections except `references`.
- **references.json**: Contains the private `references` section.
Under that scenario, `hackmyresume BUILD resume-a.json resume-b.json` would
## The HackMyResume terminal color scheme is giving me a headache. Can I disable it?
Yes. Use the `--no-color` option to disable terminal colors:

View File

@ -6,6 +6,25 @@ module.exports = function (grunt) {
pkg: grunt.file.readJSON('package.json'),
copy: {
main: {
expand: true,
cwd: 'src',
src: ['**/*','!**/*.coffee'],
dest: 'dist/',
}
},
coffee: {
main: {
expand: true,
cwd: 'src',
src: ['**/*.coffee'],
dest: 'dist/',
ext: '.js'
}
},
simplemocha: {
options: {
globals: ['expect', 'should'],
@ -17,7 +36,20 @@ module.exports = function (grunt) {
all: { src: ['test/*.js'] }
},
clean: ['test/sandbox'],
jsdoc : {
dist : {
src: ['src/**/*.js'],
options: {
private: true,
destination: 'doc'
}
}
},
clean: {
test: ['test/sandbox'],
dist: ['dist']
},
yuidoc: {
compile: {
@ -27,7 +59,6 @@ module.exports = function (grunt) {
url: '<%= pkg.homepage %>',
options: {
paths: 'src/',
//themedir: 'path/to/custom/theme/',
outdir: 'docs/'
}
}
@ -38,22 +69,39 @@ module.exports = function (grunt) {
laxcomma: true,
expr: 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( ['yuidoc'] ); });
grunt.registerTask('default', [ 'test', 'yuidoc' ]);
// 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) 2015 James M. Devlin (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

115
README.md
View File

@ -10,7 +10,7 @@ shell. Author in clean Markdown and JSON, export to Word, HTML, PDF, LaTeX,
plain text, and other arbitrary formats. Fight the power, save trees. Compatible
with [FRESH][fresca] and [JRS][6] resumes.*
![](assets/resume-bouqet.png)
![](assets/hackmyresume.cli.1.6.0.png)
HackMyResume is a dev-friendly, local-only Swiss Army knife for resumes and CVs.
Use it to:
@ -107,7 +107,7 @@ Use it when you need to submit, upload, print, or email resumes in specific
formats.
```bash
# hackmyresume BUILD <INPUTS> TO <OUTPUTS> [-t THEME]
# hackmyresume BUILD <INPUTS...> TO <OUTPUTS...> [-t THEME]
hackmyresume BUILD resume.json TO out/resume.all
hackmyresume BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all
```
@ -115,7 +115,7 @@ formats.
- **new** creates a new resume in FRESH or JSON Resume format.
```bash
# hackmyresume NEW <OUTPUTS> [-f <FORMAT>]
# hackmyresume NEW <OUTPUTS...> [-f <FORMAT>]
hackmyresume NEW resume.json
hackmyresume NEW resume.json -f fresh
hackmyresume NEW r1.json r2.json -f jrs
@ -123,12 +123,18 @@ formats.
- **analyze** inspects your resume for keywords, duration, and other metrics.
```bash
# hackmyresume ANALYZE <INPUTS...>
hackmyresume ANALYZE resume.json
hackmyresume ANALYZE r1.json r2.json
```
- **convert** converts your source resume between FRESH and JSON Resume
formats. Use it to convert between the two formats to take advantage of tools
and services.
```bash
# hackmyresume CONVERT <INPUTS> TO <OUTPUTS>
# hackmyresume CONVERT <INPUTS...> TO <OUTPUTS...>
hackmyresume CONVERT resume.json TO resume-jrs.json
hackmyresume CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json
```
@ -137,11 +143,22 @@ and services.
Resume schema. Use it to make sure your resume data is sufficient and complete.
```bash
# hackmyresume VALIDATE <INPUTS>
# hackmyresume VALIDATE <INPUTS...>
hackmyresume VALIDATE resume.json
hackmyresume VALIDATE r1.json r2.json r3.json
```
- **peek** echoes your resume or any field, property, or object path on your
resume to standard output.
```bash
# hackmyresume PEEK <INPUTS...> [OBJECT-PATH]
hackmyresume PEEK rez.json # Echo the whole resume
hackmyresume PEEK rez.json info.brief # Echo the "info.brief" field
hackmyresume PEEK rez.json employment.history[1] # Echo the 1st job
hackmyresume PEEK rez.json rez2.json info.brief # Compare value
```
## Supported Output Formats
HackMyResume supports these output formats:
@ -163,7 +180,7 @@ image | .png, .bmp | Forthcoming.
## Use
Assuming you've got a JSON-formatted resume handy, generating resumes in
different formats and combinations easy. Just run:
different formats and combinations is easy. Just run:
```bash
hackmyresume BUILD <INPUTS> <OUTPUTS> [-t theme].
@ -209,24 +226,36 @@ Generating YAML resume: out/resume.yml
### Applying a theme
HackMyResume can work with any FRESH or JSON Resume theme. To specify a theme
when generating your resume, use the `-t` or `--theme` parameter:
HackMyResume can work with any FRESH or JSON Resume theme (the latter must be
installed first). To specify a theme when generating your resume, use the `-t`
or `--theme` parameter:
```bash
hackmyresume BUILD resume.json TO out/rez.all -t [theme]
```
The `[theme]` parameter can be the name of a predefined theme or the path to any
The `[theme]` parameter can be the name of a predefined theme OR the path to any
FRESH or JSON Resume theme folder:
```bash
hackmyresume BUILD resume.json TO out/rez.all -t modern
hackmyresume BUILD resume.json TO OUT.rez.all -t ../some-folder/my-custom-theme/
hackmyresume BUILD resume.json TO OUT.rez.all -t npm_modules/jsonresume-theme-classy
hackmyresume BUILD resume.json TO OUT.rez.all -t node_modules/jsonresume-theme-classy
```
As of v1.4.0, available predefined themes are `positive`, `modern`, `compact`,
`minimist`, and `hello-world`.
FRESH themes are currently pre-installed with HackMyResume. JSON Resume themes
can be installed prior to use:
```bash
# Install a JSON Resume theme into a local node_modules subfolder:
npm install jsonresume-theme-[name]
# Use it with HackMyResume
hackmyresume build resume.json -t node_modules/jsonresume-theme-[name]
```
As of v1.6.0, available predefined FRESH themes are `positive`, `modern`,
`compact`, `minimist`, and `hello-world`. For a list of JSON Resume themes,
check the [NPM Registry](https://www.npmjs.com/search?q=jsonresume-theme).
### Merging resumes
@ -279,6 +308,39 @@ hackmyresume BUILD me.json TO out/resume.all
`out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and
`out/resume.json`.
### Building PDFs
*Users who don't care about PDFs can turn off PDF generation across all themes
and formats with the `--pdf none` switch.*
HackMyResume takes a unique approach to PDF generation. Instead of enforcing
a specific PDF engine on users, HackMyResume will attempt to work with whatever
PDF engine you have installed through the engine's command-line interface (CLI).
Currently that means one or both of...
- [wkhtmltopdf][3]
- [Phantom.js][3]
..with support for other engines planned in the future. But for now, **one or
both of these engines must be installed and accessible on your PATH in order to
generate PDF resumes with HackMyResume**. That means you should be able to
invoke either of these tools directly from your shell or terminal without error:
```bash
wkhtmltopdf input.html output.pdf
phantomjs script.js input.html output.pdf
```
Assuming you've installed one or both of these engines on your system, you can
tell HackMyResume which flavor of PDF generation to use via the `--pdf` option
(`-p` for short):
```bash
hackmyresume BUILD resume.json TO out.all --pdf phantom
hackmyresume BUILD resume.json TO out.all --pdf wkhtmltopdf
hackmyresume BUILD resume.json TO out.all --pdf none
```
### Analyzing
HackMyResume can analyze your resume for keywords, employment gaps, and other
@ -292,7 +354,7 @@ Depending on the HackMyResume version, you should see output similar to:
```
*** HackMyResume v1.4.1 ***
*** HackMyResume v1.6.0 ***
Reading resume: resume.json
Analyzing FRESH resume: resume.json
@ -388,7 +450,7 @@ hackmyresume VALIDATE resumeA.json resumeB.json
HackMyResume will validate each specified resume in turn:
```bash
*** HackMyResume v0.9.0 ***
*** HackMyResume v1.6.0 ***
Validating JSON resume: resumeA.json (INVALID)
Validating JSON resume: resumeB.json (VALID)
```
@ -407,10 +469,17 @@ where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and
autodetect the format (FRESH or JRS) of each input resume and convert it to the
other format (JRS or FRESH).
### External options
### File-based Options
Starting in v1.4.x you can pass options into HackMyResume via an external
options or ".hackmyrc" file.
You can pass options into HackMyResume via an external options or ".hackmyrc"
file with the `--options` or `-o` switch:
```bash
hackmyresume BUILD resume.json -o path/to/options.json
```
The options file can contain any documented HackMyResume option, including
`theme`, `silent`, `debug`, `pdf`, `css`, and other settings.
```javascript
{
@ -423,6 +492,18 @@ options or ".hackmyrc" file.
}
```
If a particular option is specified both on the command line and in an external
options file, the explicit command-line option takes precedence.
```bash
# path/to/options.json specifes the POSITIVE theme
# -t parameter specifies the COMPACT theme
# The -t parameter wins.
hackmyresume BUILD resume.json -o path/to/options.json -t compact
> Reading resume: resume.json
> Applying COMPACT theme (7 formats)
```
### Prettifying
HackMyResume applies [js-beautify][10]-style HTML prettification by default to

107
ROADMAP.md Normal file
View File

@ -0,0 +1,107 @@
Development Roadmap
===================
## Short-Term
### FluentCV Desktop: Beta 1
The **FluentCV Desktop 1.0 beta release** will present HackMyResume
functionality in a cross-platform desktop application for OS X, Linux, and
Windows.
### GitHub Integration
HackMyResume will offer GitHub integration for versioned resume storage and
retrieval via the `COMMIT` or `STORE` command(s) starting in 1.7.0 or 1.8.0.
### fresh-themes 1.0.0
The **fresh-themes 1.0** release will bring 100% coverage of the FRESH and JRS
object models&mdash;all resume sections and fields&mdash;along with
documentation, theme developer's guide, new themes, and a freeze to the FRESH
theme structure.
### Better LaTeX support
Including Markdown-to-LaTeX translation and more LaTeX-driven themes / formats.
### StackOverflow and LinkedIn support
Will start appearing in v1.7.0, with incremental improvements in 1.8.0 and
beyond.
### Improved resume sorting and arranging
**Better resume sorting** of items and sections: ascending, descending, by
date or other criteria ([#67][i67]).
### Remote resume / theme loading
Support remote loading of themes and resumes over `http`, `https`, and
`git://`. Enable these usage patterns:
```bash
hackmyresume build https://somesite.com/my-resume.json -t informatic
hackmyresume build resume.json -t npm:fresh-theme-ergonomic
hackmyresume analyze https://github.com/foo/my-resume
```
### 100% code coverage
Should reduce certain classes of errors and allow HMR to display a nifty 100%
code coverage badge.
### Improved **documentation and samples**
Expanded documentation and samples throughout.
## Mid-Term
### Cover letters and job descriptions
Add support for schema-driven **cover letters** and **job descriptions**.
### Character Sheets
HackMyResume 2.0 will ship with support for, yes, RPG-style character sheets.
This will demonstrate the tool's ability to flow arbitrary JSON to concrete
document(s) and provide unique albeit niche functionality around various games
([#117][i117]).
### Rich text (.rtf) output formats
Basic support for **rich text** `.rtf` output formats.
### Investigate: groff support
Investigate adding [**groff**][groff] support, because that would, indeed, be
[dope][d] ([#37][i37]).
### Investigate: org-mode support
Investigate adding [**org mode**][om] support ([#38][i38]).
### Investigate: Scribus
Investigate adding [**Scribus SLA**][scri] support ([#54][i54]).
### Support JSON Resume 1.0.0
When released.
## Long-Term
- TBD
[groff]: http://www.gnu.org/software/groff/
[om]: http://orgmode.org/
[scri]: https://en.wikipedia.org/wiki/Scribus
[d]: https://github.com/hacksalot/HackMyResume/issues/37#issue-123818674
[i37]: https://github.com/hacksalot/HackMyResume/issues/37
[i38]: https://github.com/hacksalot/HackMyResume/issues/38
[i54]: https://github.com/hacksalot/HackMyResume/issues/54
[i67]: https://github.com/hacksalot/HackMyResume/issues/67
[i107]: https://github.com/hacksalot/HackMyResume/issues/107
[i117]: https://github.com/hacksalot/HackMyResume/issues/117

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 83 KiB

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"}}

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

@ -0,0 +1,234 @@
/**
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 (ex.quit || objError.quit) {
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);
}
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) {
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);
msg = printf(M2C(this.msgs.parseError.msg, 'red'), se.line, se.col);
} else if (ex.inner && ex.inner.line !== void 0 && ex.inner.col !== void 0) {
msg = printf(M2C(this.msgs.parseError.msg, 'red'), ex.inner.line, ex.inner.col);
} else {
msg = ex;
}
etype = 'error';
}
return {
msg: msg,
withStack: withStack,
quit: quit,
etype: etype
};
};
}).call(this);

22
dist/cli/index.js vendored 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 );
}

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

@ -0,0 +1,284 @@
/**
Definition of the `main` function.
@module cli/main
@license MIT. See LICENSE.md for details.
*/
(function() {
var Command, EXTEND, FS, HME, HMR, HMSTATUS, OUTPUT, PAD, PATH, PKG, StringUtils, _, _opts, _out, _title, chalk, execute, initOptions, initialize, loadOptions, logMsg, main, 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;
_opts = {};
_title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***');
_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).
*/
main = module.exports = function(rawArgs) {
var args, initInfo, program;
initInfo = initialize(rawArgs);
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) {
var o;
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('');
}
if (o.verb && !HMR.verbs[o.verb] && !HMR.alias[o.verb]) {
throw {
fluenterror: HMSTATUS.invalidCommand,
quit: true,
attempted: o.orgVerb
};
}
Command.prototype.missingArgument = function(name) {
if (this.name() !== 'new') {
throw {
fluenterror: HMSTATUS.resumeNotFound,
quit: 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, 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';
});
isMono = _.some(args, function(v) {
return v === '--no-color';
});
return {
color: !isMono,
debug: isDebug,
silent: isSilent,
orgVerb: oVerb,
verb: verb,
json: oJSON,
args: args
};
};
/* Invoke a HackMyResume verb. */
execute = function(src, dst, opts, log) {
var hand, v;
loadOptions.call(this, opts, this.parent.jsonArgs);
hand = require('./error');
hand.init(_opts.debug, _opts.assert, _opts.silent);
v = new HMR.verbs[this.name()]();
_opts.errHandler = v;
_out.init(_opts);
v.on('hmr:status', function() {
return _out["do"].apply(_out, arguments);
});
v.on('hmr:error', function() {
return hand.err.apply(hand, arguments);
});
v.invoke.call(v, src, dst, _opts, log);
if (v.errorCode) {
console.log('Exiting with error code ' + v.errorCode);
return 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.
*/
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);

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

@ -0,0 +1,17 @@
/**
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);

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

@ -0,0 +1,98 @@
events:
begin:
msg: Invoking **%s** command.
beforeCreate:
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"
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.
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.

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

@ -0,0 +1,157 @@
/**
Output routines for HackMyResume.
@license MIT. See LICENSE.md for details.
@module cli/out
*/
(function() {
var Class, EXTEND, FS, HANDLEBARS, HME, LO, M2C, OutputHandler, PATH, YAML, _, chalk, dbgStyle, pad, printf;
chalk = require('chalk');
HME = require('../core/event-codes');
_ = require('underscore');
Class = require('../utils/class.js');
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. */
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) {
var finished;
msg = msg || '';
printf = require('printf');
finished = printf.apply(printf, arguments);
return this.opts.silent || console.log(finished);
},
"do": function(evt) {
var L, WRAP, info, msg, 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.beforeCreate:
L(M2C(this.msgs.beforeCreate.msg, '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 = 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) {
return _.each(evt.errors, function(err, idx) {
return 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) {
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);
}
}
}
});
}).call(this);

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.

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

@ -0,0 +1,71 @@
/**
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);

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

@ -0,0 +1,60 @@
/*
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);

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

@ -0,0 +1,18 @@
/*
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);

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": [""]
}]
}

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

@ -0,0 +1,39 @@
/*
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
};
}).call(this);

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

@ -0,0 +1,105 @@
/**
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);

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

@ -0,0 +1,519 @@
/**
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 FreshResume from file. */
FreshResume.prototype.open = function(file, opts) {
var raw, ret;
raw = FS.readFileSync(file, 'utf8');
ret = this.parse(raw, opts);
this.imp.file = file;
return ret;
};
/** 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'));
};
/**
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);

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

@ -0,0 +1,279 @@
/**
Definition of the FRESHTheme class.
@module core/fresh-theme
@license MIT. See LICENSE.md for details.
*/
(function() {
var EXTEND, FRESHTheme, FS, HMSTATUS, PATH, READFILES, _, friendlyName, loadExplicit, loadImplicit, 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');
/*
The FRESHTheme class is a representation of a FRESH theme
asset. See also: JRSTheme.
@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);
});
}
if (!!this.formats) {
formatsHash = loadExplicit.call(this, formatsHash);
this.explicit = true;
} else {
formatsHash = loadImplicit.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 the theme implicitly, by scanning the theme folder for files. TODO:
Refactor duplicated code with loadExplicit.
*/
loadImplicit = function(formatsHash) {
var fmts, major, that, tplFolder;
that = this;
major = false;
tplFolder = PATH.join(this.folder, 'src');
fmts = READFILES(tplFolder).map(function(absPath) {
var idx, isMajor, obj, outFmt, pathInfo, portion, reg, res;
pathInfo = parsePath(absPath);
outFmt = '';
isMajor = false;
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];
} else {
that.partials = that.partials || [];
that.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);
isMajor = true;
}
formatsHash[outFmt] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
};
obj = {
action: 'transform',
path: absPath,
major: isMajor,
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;
});
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 the theme explicitly, by following the 'formats' hash
in the theme's JSON settings file.
*/
loadExplicit = function(formatsHash) {
var act, fmts, that, tplFolder;
tplFolder = PATH.join(this.folder, 'src');
act = null;
that = this;
fmts = READFILES(tplFolder).map(function(absPath) {
var absPathSafe, idx, obj, outFmt, pathInfo, portion, reg, res;
act = null;
pathInfo = parsePath(absPath);
absPathSafe = absPath.trim().toLowerCase();
outFmt = _.find(Object.keys(that.formats), function(fmtKey) {
var fmtVal;
fmtVal = that.formats[fmtKey];
return _.some(fmtVal.transform, function(fpath) {
var absPathB;
absPathB = PATH.join(that.folder, fpath).trim().toLowerCase();
return absPathB === absPathSafe;
});
});
if (outFmt) {
act = 'transform';
}
if (!outFmt) {
portion = pathInfo.dirname.replace(tplFolder, '');
if (portion && portion.trim()) {
reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
res = reg.exec(portion);
res && (outFmt = res[1]);
}
}
if (!outFmt) {
idx = pathInfo.name.lastIndexOf('-');
outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx + 1);
}
formatsHash[outFmt] = formatsHash[outFmt] || {
outFormat: outFmt,
files: [],
symLinks: that.formats[outFmt].symLinks
};
obj = {
action: act,
orgPath: PATH.relative(that.folder, absPath),
path: 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;
});
this.cssFiles = fmts.filter(function(fmt) {
return fmt.ext === 'css';
});
this.cssFiles.forEach(function(cssf) {
var idx;
idx = _.findIndex(fmts, function(fmt) {
return fmt.pre === cssf.pre && fmt.ext === 'html';
});
fmts[idx].css = cssf.data;
return fmts[idx].cssPath = cssf.path;
});
fmts = fmts.filter(function(fmt) {
return fmt.ext !== 'css';
});
return formatsHash;
};
/*
Return a more friendly name for certain formats.
TODO: Refactor
*/
friendlyName = function(val) {
var friendly;
val = val.trim().toLowerCase();
friendly = {
yml: 'yaml',
md: 'markdown',
txt: 'text'
};
return friendly[val] || val;
};
module.exports = FRESHTheme;
}).call(this);

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

@ -0,0 +1,431 @@
/**
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, format;
extend1(JRSResume, superClass);
function JRSResume() {
return JRSResume.__super__.constructor.apply(this, arguments);
}
/** Initialize the JSResume from file. */
JRSResume.prototype.open = function(file, opts) {
var raw, ret;
raw = FS.readFileSync(file, 'utf8');
ret = this.parse(raw, opts);
this.imp.file = file;
return ret;
};
/** 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. */
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().open(PATH.join(__dirname, 'empty-jrs.json'), 'Empty');
};
/**
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);

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

@ -0,0 +1,105 @@
/**
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,
major: true,
ext: 'html',
css: null
}
]
},
pdf: {
outFormat: 'pdf',
files: [
{
action: 'transform',
render: this.render,
major: 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);

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

@ -0,0 +1,127 @@
/**
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 ex, 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) {
ex = {
fluenterror: rawData ? HACKMYSTATUS.parseError : HACKMYSTATUS.readError,
inner: _error,
raw: rawData,
file: fileName,
shouldExit: false
};
opts.quit && (ex.quit = true);
eve && eve.err(ex.fluenterror, ex);
if (opts["throw"]) {
throw ex;
}
return ex;
}
};
}).call(this);

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."
}
}
}
}
}
}

View File

@ -1,11 +1,11 @@
/**
Status codes for HackMyResume.
@module status-codes.js
@module core/status-codes
@license MIT. See LICENSE.MD for details.
*/
(function(){
*/
(function() {
module.exports = {
success: 0,
themeNotFound: 1,
@ -20,7 +20,18 @@ Status codes for HackMyResume.
missingPackageJSON: 10,
invalid: 11,
invalidFormat: 12,
notOnPath: 13
notOnPath: 13,
readError: 14,
parseError: 15,
fileSaveError: 16,
generateError: 17,
invalidHelperUse: 18,
mixedMerge: 19,
invokeTemplate: 20,
compileTemplate: 21,
themeLoad: 22,
invalidParamCount: 23,
missingParam: 24
};
}());
}).call(this);

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

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

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

@ -0,0 +1,42 @@
/**
Definition of the HTMLGenerator class.
@license MIT. See LICENSE.md for details.
@module html-generator.js
*/
(function() {
var FS, HTML, HtmlGenerator, PATH, TemplateGenerator;
TemplateGenerator = require('./template-generator');
FS = require('fs-extra');
HTML = require('html');
PATH = require('path');
require('string.prototype.endswith');
HtmlGenerator = module.exports = TemplateGenerator.extend({
init: function() {
return this._super('html');
},
/**
Copy satellite CSS files to the destination and optionally pretty-print
the HTML resume prior to saving.
*/
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;
}
}
});
}).call(this);

View File

@ -0,0 +1,98 @@
/**
Definition of the HtmlPdfCLIGenerator class.
@module html-pdf-generator.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var FS, HTML, HtmlPdfCLIGenerator, PATH, SLASH, SPAWN, TemplateGenerator, engines;
TemplateGenerator = require('./template-generator');
FS = require('fs-extra');
HTML = require('html');
PATH = require('path');
SPAWN = require('../utils/safe-spawn');
SLASH = require('slash');
/**
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.
*/
HtmlPdfCLIGenerator = module.exports = TemplateGenerator.extend({
init: function() {
return this._super('pdf', 'html');
},
/** Generate the binary PDF. */
onBeforeSave: function(info) {
var ex, safe_eng;
try {
safe_eng = info.opts.pdf || 'wkhtmltopdf';
if (safe_eng !== 'none') {
engines[safe_eng].call(this, info.mk, info.outputFile);
return null;
}
} catch (_error) {
ex = _error;
if (ex.inner && ex.inner.code === 'ENOENT') {
throw {
fluenterror: this.codes.notOnPath,
inner: ex.inner,
engine: ex.cmd,
stack: ex.inner && ex.inner.stack
};
} else {
throw {
fluenterror: this.codes.pdfGeneration,
inner: ex,
stack: ex.stack
};
}
}
}
});
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) {
var info, tempFile;
tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
return info = SPAWN('wkhtmltopdf', [tempFile, fOut]);
},
/**
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(/\.pdf$/i, '.pdf.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));
return info = SPAWN('phantomjs', [scriptPath, sourcePath, destPath]);
}
};
}).call(this);

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

@ -0,0 +1,64 @@
/**
Definition of the HtmlPngGenerator class.
@license MIT. See LICENSE.MD for details.
@module html-png-generator.js
*/
(function() {
var FS, HTML, HtmlPngGenerator, PATH, SLASH, SPAWN, TemplateGenerator, phantom;
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.
*/
HtmlPngGenerator = module.exports = TemplateGenerator.extend({
init: function() {
return this._super('png', 'html');
},
invoke: function(rez, themeMarkup, cssInfo, opts) {},
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);
}
});
/**
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);

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

@ -0,0 +1,45 @@
/**
Definition of the JsonGenerator class.
@license MIT. See LICENSE.md for details.
@module generators/json-generator
*/
(function() {
var BaseGenerator, FS, JsonGenerator, _;
BaseGenerator = require('./base-generator');
FS = require('fs');
_ = require('underscore');
/**
The JsonGenerator generates a JSON resume directly.
*/
JsonGenerator = module.exports = BaseGenerator.extend({
init: function() {
return this._super('json');
},
keys: ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', 'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', 'isModified', 'htmlPreview', 'safe'],
invoke: function(rez) {
var replacer;
replacer = function(key, value) {
if (_.some(this.keys, function(val) {
return key.trim() === val;
})) {
return void 0;
} else {
return value;
}
};
return JSON.stringify(rez, replacer, 2);
},
generate: function(rez, f) {
FS.writeFileSync(f, this.invoke(rez), 'utf8');
}
});
}).call(this);

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

@ -0,0 +1,38 @@
/**
Definition of the JsonYamlGenerator class.
@module json-yaml-generator.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var BaseGenerator, FS, JsonYamlGenerator, YAML;
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).
*/
JsonYamlGenerator = module.exports = BaseGenerator.extend({
init: function() {
return this._super('yml');
},
invoke: function(rez, themeMarkup, cssInfo, opts) {
return YAML.stringify(JSON.parse(rez.stringify()), Infinity, 2);
},
generate: function(rez, f, opts) {
var data;
data = YAML.stringify(JSON.parse(rez.stringify()), Infinity, 2);
return FS.writeFileSync(f, data, 'utf8');
}
});
}).call(this);

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

@ -0,0 +1,24 @@
/**
Definition of the LaTeXGenerator class.
@license MIT. See LICENSE.md for details.
@module generators/latex-generator
*/
(function() {
var LaTeXGenerator, TemplateGenerator;
TemplateGenerator = require('./template-generator');
/**
LaTeXGenerator generates a LaTeX resume via TemplateGenerator.
*/
LaTeXGenerator = module.exports = TemplateGenerator.extend({
init: function() {
return this._super('latex', 'tex');
}
});
}).call(this);

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

@ -0,0 +1,24 @@
/**
Definition of the MarkdownGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module markdown-generator.js
*/
(function() {
var MarkdownGenerator, TemplateGenerator;
TemplateGenerator = require('./template-generator');
/**
MarkdownGenerator generates a Markdown-formatted resume via TemplateGenerator.
*/
MarkdownGenerator = module.exports = TemplateGenerator.extend({
init: function() {
return this._super('md', 'txt');
}
});
}).call(this);

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

@ -0,0 +1,243 @@
/**
Definition of the TemplateGenerator class. TODO: Refactor
@license MIT. See LICENSE.md for details.
@module template-generator.js
*/
(function() {
var BaseGenerator, EXTEND, FRESHTheme, FS, JRSTheme, MD, MKDIRP, PATH, TemplateGenerator, XML, _, _defaultOpts, _reg, freeze, parsePath, unfreeze;
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
*/
TemplateGenerator = module.exports = BaseGenerator.extend({
/** Constructor. Set the output format and template format for this
generator. Will usually be called by a derived generator such as
HTMLGenerator or MarkdownGenerator.
*/
init: function(outputFormat, templateFormat, cssFile) {
this._super(outputFormat);
this.tplFormat = templateFormat || outputFormat;
},
/** 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.
*/
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;
trx = this.single(rez, tplInfo.data, this.format, opts, opts.themeObj, curFmt);
if (tplInfo.ext === 'css') {
curFmt.files[idx].data = trx;
} else {
tplInfo.ext === 'html';
}
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.
*/
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 fileName, thisFilePath;
file.info.orgPath = file.info.orgPath || '';
thisFilePath = PATH.join(outFolder, file.info.orgPath);
if (this.onBeforeSave) {
file.data = this.onBeforeSave({
theme: opts.themeObj,
outputFile: file.info.major ? f : thisFilePath,
mk: file.data,
opts: this.opts
});
if (!file.data) {
return;
}
}
fileName = file.info.major ? f : thisFilePath;
MKDIRP.sync(PATH.dirname(fileName));
FS.writeFileSync(fileName, file.data, {
encoding: 'utf8',
flags: 'w'
});
if (this.onAfterSave) {
return this.onAfterSave({
outputFile: fileName,
mk: file.data,
opts: this.opts
});
}
}, this);
if (curFmt.symLinks) {
Object.keys(curFmt.symLinks).forEach(function(loc) {
var absLoc, absTarg, ref, type;
absLoc = PATH.join(outFolder, loc);
absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
type = (ref = parsePath(absLoc).extname) != null ? ref : {
'file': 'junction'
};
return FS.symlinkSync(absTarg, absLoc, type);
});
}
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.
*/
single: 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;
}
});
/** Export the TemplateGenerator function/ctor. */
module.exports = TemplateGenerator;
/** 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);

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

@ -0,0 +1,24 @@
/**
Definition of the TextGenerator class.
@license MIT. See LICENSE.md for details.
@module text-generator.js
*/
(function() {
var TemplateGenerator, TextGenerator;
TemplateGenerator = require('./template-generator');
/**
The TextGenerator generates a plain-text resume via the TemplateGenerator.
*/
TextGenerator = module.exports = TemplateGenerator.extend({
init: function() {
return this._super('txt');
}
});
}).call(this);

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

@ -0,0 +1,19 @@
/*
Definition of the WordGenerator class.
@license MIT. See LICENSE.md for details.
@module generators/word-generator
*/
(function() {
var TemplateGenerator, WordGenerator;
TemplateGenerator = require('./template-generator');
WordGenerator = module.exports = TemplateGenerator.extend({
init: function() {
return this._super('doc', 'xml');
}
});
}).call(this);

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

@ -0,0 +1,24 @@
/**
Definition of the XMLGenerator class.
@license MIT. See LICENSE.md for details.
@module generatprs/xml-generator
*/
(function() {
var BaseGenerator, XMLGenerator;
BaseGenerator = require('./base-generator');
/**
The XmlGenerator generates an XML resume via the TemplateGenerator.
*/
XMLGenerator = module.exports = BaseGenerator.extend({
init: function() {
return this._super('xml');
}
});
}).call(this);

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

@ -0,0 +1,24 @@
/**
Definition of the YAMLGenerator class.
@module yaml-generator.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var TemplateGenerator, YAMLGenerator;
TemplateGenerator = require('./template-generator');
/**
YamlGenerator generates a YAML-formatted resume via TemplateGenerator.
*/
YAMLGenerator = module.exports = TemplateGenerator.extend({
init: function() {
return this._super('yml', 'yml');
}
});
}).call(this);

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

@ -0,0 +1,64 @@
/**
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);

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

@ -0,0 +1,616 @@
/**
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 = {
/**
Convert the input date to a specified format through Moment.js.
If date is invalid, will return the time provided by the user,
or default to the fallback param or 'Present' if that is set to true
@method formatDate
*/
formatDate: function(datetime, format, fallback) {
var momentDate, ref, ref1;
if (moment) {
momentDate = moment(datetime);
if (momentDate.isValid()) {
return momentDate.format(format);
}
}
return datetime || ((ref = typeof fallback === 'string') != null ? ref : {
fallback: (ref1 = fallback === true) != null ? ref1 : {
'Present': null
}
});
},
/**
Given a resume sub-object with a start/end date, format a representation of
the date range.
@method dateRange
*/
dateRange: function(obj, fmt, sep, fallback, options) {
if (!obj) {
return '';
}
return _fromTo(obj.start, obj.end, fmt, sep, fallback, options);
},
/**
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;
}
},
/**
Return true if the section is present on the resume and has at least one
element.
@method section
*/
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 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.
@method section
*/
camelCase: function(val) {
val = (val && val.trim()) || '';
if (val) {
return val.charAt(0).toUpperCase() + val.slice(1);
} else {
return val;
}
},
/**
Return true if the context has the property or subpropery.
@method has
*/
has: function(title, options) {
title = title && title.trim().toLowerCase();
if (LO.get(this.r, title)) {
return options.fn(this);
}
},
/**
Generic template helper function to 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.
@method wpml
*/
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 '';
}
},
/**
Return true if either value is truthy.
@method either
*/
either: function(lhs, rhs, options) {
if (lhs || rhs) {
return options.fn(this);
}
},
/**
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("Handlerbars 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("Handlerbars Helper 'compare' doesn't know the operator " + operator);
}
result = operators[operator](lvalue, rvalue);
if (result) {
return options.fn(this);
} else {
return options.inverse(this);
}
}
};
/**
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 || 'Current';
} 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);

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

@ -0,0 +1,29 @@
/**
Template helper definitions for Handlebars.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module handlebars-helpers.js
*/
(function() {
var HANDLEBARS, _, helpers;
HANDLEBARS = require('handlebars');
_ = require('underscore');
helpers = require('./generic-helpers');
/**
Register useful Handlebars helpers.
@method registerHelpers
*/
module.exports = function(theme, opts) {
helpers.theme = theme;
helpers.opts = opts;
return HANDLEBARS.registerHelper(helpers);
};
}).call(this);

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

@ -0,0 +1,36 @@
/**
Template helper definitions for Underscore.
@license MIT. Copyright (c) 2016 hacksalot (https://github.com/hacksalot)
@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;
return _.each(helpers, function(hVal, hKey) {
if (_.isFunction(hVal)) {
return _.bind(hVal, ctx);
}
}, this);
};
}).call(this);

View File

@ -1,25 +1,26 @@
/**
External API surface for HackMyResume.
@license MIT. See LICENSE.md for details.
@module hackmyapi.js
*/
@module hackmycore/index
*/
/**
API facade for HackMyCore.
*/
(function() {
var HackMyCore;
/**
The formal HackMyResume API.
*/
var HackMyAPI = module.exports = {
HackMyCore = module.exports = {
verbs: {
build: require('./verbs/build'),
analyze: require('./verbs/analyze'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
new: require('./verbs/create')
"new": require('./verbs/create'),
peek: require('./verbs/peek')
},
alias: {
generate: require('./verbs/build'),
@ -45,6 +46,4 @@ External API surface for HackMyResume.
HtmlPngGenerator: require('./generators/html-png-generator')
};
}());
}).call(this);

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

@ -0,0 +1,138 @@
/**
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);

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

@ -0,0 +1,61 @@
/**
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);

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

@ -0,0 +1,49 @@
/**
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);

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

@ -0,0 +1,100 @@
/**
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: template ? HMSTATUS.invokeTemplate : HMSTATUS.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);

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

@ -0,0 +1,59 @@
/**
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);

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

@ -0,0 +1,60 @@
/**
Definition of the UnderscoreGenerator class.
@license MIT. See LICENSE.md for details.
@module underscore-generator.js
*/
(function() {
var HMSTATUS, UnderscoreGenerator, _, registerHelpers;
_ = require('underscore');
registerHelpers = require('../helpers/underscore-helpers');
HMSTATUS = require('../core/status-codes');
/**
Perform template-based resume generation using Underscore.js.
@class UnderscoreGenerator
*/
UnderscoreGenerator = module.exports = {
generateSimple: function(data, tpl) {
var template;
try {
template = _.template(tpl);
return template(data);
} catch (_error) {
throw {
fluenterror: template ? HMSTATUS.invokeTemplate : HMSTATUS.compileTemplate,
inner: _error
};
}
},
generate: function(json, jst, format, cssInfo, opts, theme) {
var ctx, delims;
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;
jst = jst.replace(delims.comment, '');
ctx = {
r: format === 'html' || format === 'pdf' || format === 'png' ? json.markdownify() : json,
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);

72
dist/utils/class.js vendored Normal file
View File

@ -0,0 +1,72 @@
/**
Definition of John Resig's `Class` class.
@module class.js
*/
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
* http://ejohn.org/blog/simple-javascript-inheritance/
*/
// Inspired by base2 and Prototype
(function(){
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function(){};
module.exports = Class;
// Create a new Class that inherits from this class
Class.extend = function(prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) : // jshint ignore:line
prop[name];
}
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.prototype.constructor = Class;
// And make this class extendable
Class.extend = arguments.callee;
return Class;
};
})();

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

@ -0,0 +1,12 @@
/**
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);

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

@ -0,0 +1,61 @@
/**
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);

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

@ -0,0 +1,28 @@
/**
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);

View File

@ -1,12 +1,12 @@
// Exemplar script for generating documents with Phantom.js.
// https://raw.githubusercontent.com/ariya/phantomjs/master/examples/rasterize.js
(function() {
"use strict";
var page = require('webpage').create(),
system = require('system'),
address, output, size;
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]');
@ -17,35 +17,56 @@
} else {
address = system.args[1];
output = system.args[2];
page.viewportSize = { width: 600, height: 600 };
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' };
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 };
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); // it's as good an assumption as any
console.log ("pageHeight:",pageHeight);
page.viewportSize = { width: pageWidth, height: pageHeight };
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) {
page.open(address, function(status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit(1);
} else {
window.setTimeout(function () {
return window.setTimeout(function() {
page.render(output);
phantom.exit();
}, 200);
@ -53,4 +74,4 @@
});
}
}());
}).call(this);

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

@ -0,0 +1,32 @@
/**
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);

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.
*/
(function() {
module.exports = function(cmd, args, isSync, callback) {
var info, spawn;
try {
spawn = require('child_process')[isSync ? 'spawnSync' : 'spawn'];
info = spawn(cmd, args);
if (!isSync) {
info.on('error', function(err) {
if (callback != null) {
callback(err);
} else {
throw {
cmd: cmd,
inner: err
};
}
});
} else {
if (info.error) {
if (callback != null) {
callback(err);
} else {
throw {
cmd: cmd,
inner: info.error
};
}
}
}
} catch (_error) {
if (callback != null) {
return callback(_error);
} else {
throw _error;
}
}
};
}).call(this);

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

@ -0,0 +1,62 @@
/**
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);

View File

@ -1,17 +1,18 @@
/**
Definitions of string utility functions.
@module string.js
*/
@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 ) {
String.isNullOrWhitespace = function(input) {
return !input || !input.trim();
};
@ -19,8 +20,8 @@ See: http://stackoverflow.com/a/32800728/4942583
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
String.is = function( val ) {
String.is = function(val) {
return typeof val === 'string' || val instanceof String;
};
}());
}).call(this);

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

@ -0,0 +1,39 @@
/**
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(ex, rawData) {
var JSONLint, colNum, lineNum, lint;
lineNum = null;
colNum = null;
JSONLint = require('json-lint');
lint = JSONLint(rawData, {
comments: false
});
this.line = lint.error ? lint.line : '???';
return this.col = lint.error ? lint.character : '???';
};
SyntaxErrorEx.is = function(ex) {
return ex instanceof SyntaxError;
};
module.exports = SyntaxErrorEx;
}).call(this);

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

@ -0,0 +1,95 @@
/**
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, _loadInspectors, analyze, chalk;
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');
AnalyzeVerb = module.exports = Verb.extend({
init: function() {
return this._super('analyze', analyze);
}
});
/**
Run the 'analyze' command.
*/
analyze = function(sources, dst, opts) {
var nlzrs;
if (!sources || !sources.length) {
throw {
fluenterror: HMSTATUS.resumeNotFound,
quit: true
};
}
nlzrs = _loadInspectors();
return _.each(sources, function(src) {
var result;
result = ResumeFactory.loadOne(src, {
format: 'FRESH',
objectify: true
}, this);
if (result.fluenterror) {
return this.setError(result.fluenterror, result);
} else {
return _analyze.call(this, result, nlzrs, opts);
}
}, this);
};
/**
Analyze a single resume.
*/
_analyze = 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);
});
return this.stat(HMEVENT.afterAnalyze, {
info: info
});
};
/**
Load inspectors.
*/
_loadInspectors = function() {
return {
totals: require('../inspectors/totals-inspector'),
coverage: require('../inspectors/gap-inspector'),
keywords: require('../inspectors/keyword-inspector')
};
};
}).call(this);

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

@ -0,0 +1,409 @@
/**
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, _, _err, _fmts, _log, _opts, _rezObj, addFreebieFormats, build, expand, extend, loadTheme, parsePath, prep, single, verifyOutputs, verifyTheme;
_ = 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. */
BuildVerb = module.exports = Verb.extend({
/** Create a new build verb. */
init: function() {
return this._super('build', build);
}
});
/**
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 ex, inv, isFRESH, mixed, newEx, orgFormat, problemSheets, rez, sheetObjects, sheets, tFolder, targets, theme, toFormat;
if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, {
quit: true
});
return null;
}
prep(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);
theme = _opts.themeObj = loadTheme(tFolder);
addFreebieFormats(theme);
} catch (_error) {
ex = _error;
newEx = {
fluenterror: HMSTATUS.themeLoad,
inner: ex,
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) {
return t.final = single.call(this, t, theme, targets);
}, this);
return {
sheet: _rezObj,
targets: targets,
processed: targets
};
};
/**
Prepare for a BUILD run.
*/
prep = function(src, dst, opts) {
_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;
(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) {
this.err(ex.fluenterror, ex);
} else {
this.err(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)) {
this.err(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);

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

@ -0,0 +1,88 @@
/**
Implementation of the 'convert' verb for HackMyResume.
@module verbs/convert
@license MIT. See LICENSE.md for details.
*/
(function() {
var ConvertVerb, HMEVENT, HMSTATUS, ResumeFactory, Verb, _, chalk, convert;
ResumeFactory = require('../core/resume-factory');
chalk = require('chalk');
Verb = require('../verbs/verb');
HMSTATUS = require('../core/status-codes');
_ = require('underscore');
HMEVENT = require('../core/event-codes');
ConvertVerb = module.exports = Verb.extend({
init: function() {
return this._super('convert', convert);
}
});
/**
Convert between FRESH and JRS formats.
*/
convert = function(srcs, dst, opts) {
if (!srcs || !srcs.length) {
throw {
fluenterror: 6,
quit: true
};
}
if (!dst || !dst.length) {
if (srcs.length === 1) {
throw {
fluenterror: HMSTATUS.inputOutputParity,
quit: true
};
} else if (srcs.length === 2) {
dst = dst || [];
dst.push(srcs.pop());
} else {
throw {
fluenterror: HMSTATUS.inputOutputParity,
quit: true
};
}
}
if (srcs && dst && srcs.length && dst.length && srcs.length !== dst.length) {
throw {
fluenterror: HMSTATUS.inputOutputParity({
quit: true
})
};
}
_.each(srcs, function(src, idx) {
var rinfo, s, srcFmt, targetFormat;
rinfo = ResumeFactory.loadOne(src, {
format: null,
objectify: true,
"throw": false
});
if (rinfo.fluenterror) {
this.err(rinfo.fluenterror, rinfo);
return;
}
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);
}, this);
};
}).call(this);

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

@ -0,0 +1,60 @@
/**
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, _, chalk, create;
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');
CreateVerb = module.exports = Verb.extend({
init: function() {
return this._super('new', create);
}
});
/**
Create a new empty resume in either FRESH or JRS format.
*/
create = function(src, dst, opts) {
if (!src || !src.length) {
throw {
fluenterror: HMSTATUS.createNameMissing,
quit: true
};
}
_.each(src, function(t) {
var RezClass, safeFmt;
safeFmt = opts.format.toUpperCase();
this.stat(HMEVENT.beforeCreate, {
fmt: safeFmt,
file: t
});
MKDIRP.sync(PATH.dirname(t));
RezClass = require('../core/' + safeFmt.toLowerCase() + '-resume');
RezClass["default"]().save(t);
return this.stat(HMEVENT.afterCreate, {
fmt: safeFmt,
file: t
});
}, this);
};
}).call(this);

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

@ -0,0 +1,70 @@
/**
Implementation of the 'peek' verb for HackMyResume.
@module verbs/peek
@license MIT. See LICENSE.md for details.
*/
(function() {
var HMEVENT, HMSTATUS, PeekVerb, Verb, _, __, peek, safeLoadJSON;
Verb = require('../verbs/verb');
_ = require('underscore');
__ = require('lodash');
safeLoadJSON = require('../utils/safe-json-loader');
HMSTATUS = require('../core/status-codes');
HMEVENT = require('../core/event-codes');
PeekVerb = module.exports = Verb.extend({
init: function() {
return this._super('peek', peek);
}
});
/** Peek at a resume, resume section, or resume field. */
peek = function(src, dst, opts) {
var objPath;
if (!src || !src.length) {
({
"throw": {
fluenterror: HMSTATUS.resumeNotFound
}
});
}
objPath = (dst && dst[0]) || '';
_.each(src, function(t) {
var errCode, obj, 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;
}
this.stat(HMEVENT.afterPeek, {
file: t,
requested: objPath,
target: tgt,
error: obj.ex
});
if (obj.ex) {
errCode = obj.ex.operation === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError;
if (errCode === HMSTATUS.readError) {
obj.ex.quiet = true;
}
this.setError(errCode, obj.ex);
return this.err(errCode, obj.ex);
}
}, this);
};
}).call(this);

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

@ -0,0 +1,102 @@
/**
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, _, chalk, safeLoadJSON, validate;
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. */
ValidateVerb = module.exports = Verb.extend({
init: function() {
return this._super('validate', validate);
}
});
/** Validate 1 to N resumes in FRESH or JSON Resume format. */
validate = function(sources, unused, opts) {
var schemas, validator;
if (!sources || !sources.length) {
throw {
fluenterror: HMSTATUS.resumeNotFoundAlt,
quit: true
};
}
validator = require('is-my-json-valid');
schemas = {
fresh: require('fresca'),
jars: require('../core/resume.json')
};
return _.map(sources, function(t) {
var errCode, errors, fmt, json, obj, ret;
ret = {
file: t,
isValid: false
};
obj = safeLoadJSON(t);
if (!obj.ex) {
json = obj.json;
fmt = json.basics ? 'jrs' : 'fresh';
errors = [];
try {
validate = validator(schemas[fmt], {
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
ret.isValid = validate(json);
if (!ret.isValid) {
errors = validate.errors;
}
} catch (_error) {
ret.ex = _error;
}
} else {
errCode = obj.ex.operation === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError;
if (errCode === HMSTATUS.readError) {
obj.ex.quiet = true;
}
this.setError(errCode, obj.ex);
this.err(errCode, obj.ex);
}
this.stat(HMEVENT.afterValidate, {
file: t,
isValid: ret.isValid,
fmt: fmt != null ? fmt.replace('jars', 'JSON Resume') : void 0,
errors: errors
});
if (opts.assert && !ret.isValid) {
throw {
fluenterror: HMSTATUS.invalid,
shouldExit: true
};
}
return ret;
}, this);
};
}).call(this);

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

@ -0,0 +1,83 @@
/**
Definition of the Verb class.
@module verbs/verb
@license MIT. See LICENSE.md for details.
*/
(function() {
var Class, EVENTS, HMEVENT, Verb;
Class = require('../utils/class');
EVENTS = require('events');
HMEVENT = require('../core/event-codes');
/**
An instantiation of a HackMyResume command.
@class Verb
*/
Verb = module.exports = Class.extend({
/** Constructor. Automatically called at creation. */
init: function(moniker, workhorse) {
this.moniker = moniker;
this.emitter = new EVENTS.EventEmitter();
this.workhorse = workhorse;
},
/** Invoke the command. */
invoke: function() {
var ret;
this.stat(HMEVENT.begin, {
cmd: this.moniker
});
ret = this.workhorse.apply(this, arguments);
this.stat(HMEVENT.end);
return ret;
},
/** Forward subscriptions to the event emitter. */
on: function() {
return this.emitter.on.apply(this.emitter, arguments);
},
/** Fire an arbitrary event, scoped to "hmr:". */
fire: function(evtName, payload) {
payload = payload || {};
payload.cmd = this.moniker;
this.emitter.emit('hmr:' + evtName, payload);
return true;
},
/** Handle an error condition. */
err: function(errorCode, payload, hot) {
payload = payload || {};
payload.sub = payload.fluenterror = errorCode;
payload["throw"] = hot;
this.fire('error', payload);
if (hot) {
throw payload;
}
return true;
},
/** Fire the 'hmr:status' error event. */
stat: function(subEvent, payload) {
payload = payload || {};
payload.sub = subEvent;
this.fire('status', payload);
return true;
},
/** Associate error info with the invocation. */
setError: function(code, obj) {
this.errorCode = code;
this.errorObj = obj;
}
});
}).call(this);

View File

@ -1,13 +1,13 @@
{
"name": "hackmyresume",
"version": "1.5.2",
"version": "1.7.4",
"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": [
@ -30,6 +30,7 @@
],
"author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
"contributors": [
"aruberto (https://github.com/aruberto)",
"jjanusch (https://github.com/driftdev)",
"robertmain (https://github.com/robertmain)",
"tomheon (https://github.com/tomheon)",
@ -41,55 +42,62 @@
"bugs": {
"url": "https://github.com/hacksalot/HackMyResume/issues"
},
"main": "src/hackmyapi.js",
"bin": {
"hackmyresume": "src/index.js"
"hackmyresume": "dist/cli/index.js"
},
"main": "src/index.js",
"homepage": "https://github.com/hacksalot/HackMyResume",
"dependencies": {
"chalk": "^1.1.1",
"commander": "^2.9.0",
"copy": "^0.1.3",
"fresca": "~0.3.0",
"fresh-resume-starter": "^0.1.1",
"fresh-themes": "~0.12.0-beta",
"fs-extra": "^0.24.0",
"extend": "^3.0.0",
"fresca": "~0.6.0",
"fresh-jrs-converter": "^0.2.0",
"fresh-resume-starter": "^0.2.2",
"fresh-themes": "^0.14.1-beta",
"fs-extra": "^0.26.4",
"handlebars": "^4.0.5",
"html": "0.0.10",
"is-my-json-valid": "^2.12.2",
"is-my-json-valid": "^2.12.4",
"json-lint": "^0.1.0",
"jst": "0.0.13",
"lodash": "^3.10.1",
"marked": "^0.3.5",
"mkdirp": "^0.5.1",
"moment": "^2.10.6",
"moment": "^2.11.1",
"parse-filepath": "^0.6.3",
"path-exists": "^2.1.0",
"printf": "^0.2.3",
"recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.2.0",
"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",
"webshot": "^0.16.0",
"word-wrap": "^1.1.0",
"xml-escape": "^1.0.0",
"yamljs": "^0.2.4"
},
"devDependencies": {
"chai": "*",
"fresh-test-resumes": "^0.2.1",
"fresh-test-resumes": "^0.6.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",
"grunt-simple-mocha": "*",
"jsonresume-theme-boilerplate": "^0.1.2",
"jsonresume-theme-classy": "^1.0.9",
"jsonresume-theme-modern": "0.0.18",
"jsonresume-theme-sceptile": "^1.0.5",
"mocha": "*",
"resample": "fluentdesk/resample"
"resample": "fluentdesk/resample",
"stripcolorcodes": "^0.1.0"
}
}

30
src/cli/analyze.hbs 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"}}

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

@ -0,0 +1,221 @@
###*
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 this.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( this[ '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 ex.quit || objError.quit
if @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 )
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
withStack = true
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
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'
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 );
}

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

@ -0,0 +1,317 @@
###*
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
_opts = { }
_title = chalk.white.bold('\n*** HackMyResume v' +PKG.version+ ' ***')
_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).
###
main = module.exports = (rawArgs) ->
initInfo = initialize( rawArgs )
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 ) ->
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('')
# 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 = (name) ->
if this.name() != 'new'
throw { fluenterror: HMSTATUS.resumeNotFound, quit: true }
# 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
isDebug = _.some( args, (v) ->
return v == '-d' || v == '--debug'
)
# Grab the --silent flag
isSilent = _.some( args, (v) ->
return v == '-s' || v == '--silent'
)
# Grab the --no-color flag
isMono = _.some args, (v) -> v == '--no-color'
return {
color: !isMono,
debug: isDebug,
silent: isSilent,
orgVerb: oVerb,
verb: verb,
json: oJSON,
args: args
}
### Invoke a HackMyResume verb. ###
execute = ( src, dst, opts, log ) ->
loadOptions.call( this, opts, this.parent.jsonArgs )
hand = require( './error' )
hand.init( _opts.debug, _opts.assert, _opts.silent )
v = new HMR.verbs[ this.name() ]()
_opts.errHandler = v
_out.init( _opts )
v.on( 'hmr:status', -> _out.do.apply( _out, arguments ) )
v.on( 'hmr:error', -> hand.err.apply( hand, arguments ) )
v.invoke.call( v, src, dst, _opts, log )
if v.errorCode
console.log 'Exiting with error code ' + 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.
###
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 )

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'

98
src/cli/msg.yml Normal file
View File

@ -0,0 +1,98 @@
events:
begin:
msg: Invoking **%s** command.
beforeCreate:
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"
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.
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.

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

@ -0,0 +1,155 @@
###*
Output routines for HackMyResume.
@license MIT. See LICENSE.md for details.
@module cli/out
###
chalk = require('chalk')
HME = require('../core/event-codes')
_ = require('underscore')
Class = require('../utils/class.js')
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. ###
OutputHandler = module.exports = Class.extend
init: ( opts ) ->
@opts = EXTEND( true, this.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.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 = if evt.isValid then 'green' else 'yellow'
L(
M2C( this.msgs.afterValidate.msg[0], 'white' ) +
chalk[style].bold(
if evt.isValid
then this.msgs.afterValidate.msg[1]
else this.msgs.afterValidate.msg[2] ),
evt.file, evt.fmt
);
if evt.errors
_.each( evt.errors, (err,idx) ->
L( chalk.yellow.bold('--> ') + chalk.yellow(err.field.replace('data.','resume.').toUpperCase() + ' ' + err.message))
, @)
when HME.afterPeek
sty = if evt.error then 'red' else ( if evt.target != undefined then 'green' else '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);

51
src/cli/use.txt 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.

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

@ -1,419 +0,0 @@
/**
FRESH to JSON Resume conversion routiens.
@license MIT. See LICENSE.md for details.
@module convert.js
*/
(function(){ // TODO: refactor everything
var _ = require('underscore');
/**
Convert between FRESH and JRS resume/CV formats.
@class FRESHConverter
*/
var FRESHConverter = module.exports = {
/**
Convert from JSON Resume format to FRESH.
@method toFresh
@todo Refactor
*/
toFRESH: function( src, foreign ) {
foreign = (foreign === undefined || foreign === null) ? true : foreign;
return {
name: src.basics.name,
imp: src.basics.imp,
info: {
label: src.basics.label,
class: src.basics.class, // <--> round-trip
image: src.basics.picture,
brief: src.basics.summary
},
contact: {
email: src.basics.email,
phone: src.basics.phone,
website: src.basics.website,
other: src.basics.other // <--> round-trip
},
meta: meta( true, src.meta ),
location: {
city: src.basics.location.city,
region: src.basics.location.region,
country: src.basics.location.countryCode,
code: src.basics.location.postalCode,
address: src.basics.location.address
},
employment: employment( src.work, true ),
education: education( src.education, true),
service: service( src.volunteer, true),
skills: skillsToFRESH( src.skills ),
writing: writing( src.publications, true),
recognition: recognition( src.awards, true, foreign ),
social: social( src.basics.profiles, true ),
interests: src.interests,
testimonials: references( src.references, true ),
languages: src.languages,
disposition: src.disposition // <--> round-trip
};
},
/**
Convert from FRESH format to JSON Resume.
@param foreign True if non-JSON-Resume properties should be included in
the result, false if those properties should be excluded.
@todo Refactor
*/
toJRS: function( src, foreign ) {
foreign = (foreign === undefined || foreign === null) ? false : foreign;
return {
basics: {
name: src.name,
label: src.info.label,
class: foreign ? src.info.class : undefined,
summary: src.info.brief,
website: src.contact.website,
phone: src.contact.phone,
email: src.contact.email,
picture: src.info.image,
location: {
address: src.location.address,
postalCode: src.location.code,
city: src.location.city,
countryCode: src.location.country,
region: src.location.region
},
profiles: social( src.social, false ),
imp: src.imp
},
work: employment( src.employment, false ),
education: education( src.education, false ),
skills: skillsToJRS( src.skills, false ),
volunteer: service( src.service, false ),
awards: recognition( src.recognition, false, foreign ),
publications: writing( src.writing, false ),
interests: src.interests,
references: references( src.testimonials, false ),
samples: foreign ? src.samples : undefined,
disposition: foreign ? src.disposition : undefined,
languages: src.languages
};
},
toSTRING: function( src ) {
function replacerJRS( key,value ) { // Exclude these keys
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate',
'result', 'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
function replacerFRESH( key,value ) { // Exclude these keys
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
return JSON.stringify( src, src.basics ? replacerJRS : replacerFRESH, 2 );
}
};
function meta( direction, obj ) {
//if( !obj ) return obj; // preserve null and undefined
if( direction ) {
obj = obj || { };
obj.format = obj.format || "FRESH@0.1.0";
obj.version = obj.version || "0.1.0";
}
return obj;
}
function employment( obj, direction ) {
if( !obj ) return obj;
if( !direction ) {
return obj && obj.history ?
obj.history.map(function(emp){
return {
company: emp.employer,
website: emp.url,
position: emp.position,
startDate: emp.start,
endDate: emp.end,
summary: emp.summary,
highlights: emp.highlights
};
}) : undefined;
}
else {
return {
history: obj && obj.length ?
obj.map( function( job ) {
return {
position: job.position,
employer: job.company,
summary: job.summary,
current: (!job.endDate || !job.endDate.trim() ||
job.endDate.trim().toLowerCase() === 'current') || undefined,
start: job.startDate,
end: job.endDate,
url: job.website,
keywords: [],
highlights: job.highlights
};
}) : undefined
};
}
}
function education( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj && obj.length ? {
level: "",
history: obj.map(function(edu){
return {
institution: edu.institution,
start: edu.startDate,
end: edu.endDate,
grade: edu.gpa,
curriculum: edu.courses,
url: edu.website || edu.url || undefined,
summary: edu.summary || "",
area: edu.area,
studyType: edu.studyType
};
})
} : undefined;
}
else {
return obj && obj.history ?
obj.history.map(function(edu){
return {
institution: edu.institution,
gpa: edu.grade,
courses: edu.curriculum,
startDate: edu.start,
endDate: edu.end,
area: edu.area,
studyType: edu.studyType
};
}) : undefined;
}
}
function service( obj, direction, foreign ) {
if( !obj ) return obj;
if( direction ) {
return {
history: obj && obj.length ? obj.map(function(vol) {
return {
type: 'volunteer',
position: vol.position,
organization: vol.organization,
start: vol.startDate,
end: vol.endDate,
url: vol.website,
summary: vol.summary,
highlights: vol.highlights
};
}) : undefined
};
}
else {
return obj && obj.history ?
obj.history.map(function(srv){
return {
flavor: foreign ? srv.flavor : undefined,
organization: srv.organization,
position: srv.position,
startDate: srv.start,
endDate: srv.end,
website: srv.url,
summary: srv.summary,
highlights: srv.highlights
};
}) : undefined;
}
}
function social( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj.map(function(pro){
return {
label: pro.network,
network: pro.network,
url: pro.url,
user: pro.username
};
});
}
else {
return obj.map( function( soc ) {
return {
network: soc.network,
username: soc.user,
url: soc.url
};
});
}
}
function recognition( obj, direction, foreign ) {
if( !obj ) return obj;
if( direction ) {
return obj && obj.length ? obj.map(
function(awd){
return {
flavor: foreign ? awd.flavor : undefined,
url: foreign ? awd.url: undefined,
title: awd.title,
date: awd.date,
from: awd.awarder,
summary: awd.summary
};
}) : undefined;
}
else {
return obj && obj.length ? obj.map(function(awd){
return {
flavor: foreign ? awd.flavor : undefined,
url: foreign ? awd.url: undefined,
title: awd.title,
date: awd.date,
awarder: awd.from,
summary: awd.summary
};
}) : undefined;
}
}
function references( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj && obj.length && obj.map(function(ref){
return {
name: ref.name,
flavor: 'professional',
quote: ref.reference,
private: false
};
});
}
else {
return obj && obj.length && obj.map(function(ref){
return {
name: ref.name,
reference: ref.quote
};
});
}
}
function writing( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj.map(function( pub ) {
return {
title: pub.name,
flavor: undefined,
publisher: pub.publisher,
url: pub.website,
date: pub.releaseDate,
summary: pub.summary
};
});
}
else {
return obj && obj.length ? obj.map(function(pub){
return {
name: pub.title,
publisher: pub.publisher &&
pub.publisher.name ? pub.publisher.name : pub.publisher,
releaseDate: pub.date,
website: pub.url,
summary: pub.summary
};
}) : undefined;
}
}
function skillsToFRESH( skills ) {
if( !skills ) return skills;
return {
sets: skills.map(function( set ) {
return {
name: set.name,
level: set.level,
skills: set.keywords
};
})
};
}
function skillsToJRS( skills ) {
if( !skills ) return skills;
var ret = [];
if( skills.sets && skills.sets.length ) {
ret = skills.sets.map(function(set){
return {
name: set.name,
level: set.level,
keywords: set.skills
};
});
}
else if( skills.list ) {
ret = skills.list.map(function(sk){
return {
name: sk.name,
level: sk.level,
keywords: sk.keywords
};
});
}
return ret;
}
}());

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

@ -1,21 +0,0 @@
(function(){
/**
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

View File

@ -1,13 +0,0 @@
(function(){
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
}
};
}());

View File

@ -1,174 +0,0 @@
/**
Error-handling routines for HackMyResume.
@module error-handler.js
@license MIT. See LICENSE.md for details.
*/
// TODO: Logging library
(function() {
var HACKMYSTATUS = require('./status-codes')
, PKG = require('../../package.json')
, FS = require('fs')
, FCMD = require('../hackmyapi')
, PATH = require('path')
, WRAP = require('word-wrap')
, chalk = require('chalk');
/**
An amorphous blob of error handling code for HackMyResume.
@class ErrorHandler
*/
var ErrorHandler = module.exports = {
init: function( debug ) {
this.debug = debug;
},
err: function( ex, shouldExit ) {
var msg = '', exitCode, log = console.log, showStack = ex.showStack;
// If the exception has been handled elsewhere and shouldExit is true,
// let's get out of here, otherwise silently return.
if( ex.handled ) {
if( shouldExit )
process.exit( exitCode );
return;
}
// Get an error message -- either a HackMyResume error message or the
// exception's associated error message
if( ex.fluenterror ){
var errInfo = get_error_msg( ex );
msg = errInfo.msg;
exitCode = ex.fluenterror;
showStack = errInfo.showStack;
}
else {
msg = ex.toString();
exitCode = -1;
// Deal with pesky 'Error:' prefix.
var idx = msg.indexOf('Error: ');
msg = idx === -1 ? msg : msg.substring( idx + 7 );
}
// Log non-HackMyResume-handled errors in red with ERROR prefix. Log HMR
// errors as-is.
ex.fluenterror ?
log( msg.toString() ) :
log( chalk.red.bold('ERROR: ' + msg.toString()) );
// Selectively show the stack trace
if( (ex.stack || (ex.inner && ex.inner.stack)) &&
((showStack && ex.code !== 'ENOENT' ) || (this.debug) ))
log( chalk.red( ex.stack || ex.stack.inner ) );
// Let the error code be the process's return code.
( shouldExit || ex.shouldExit ) && process.exit( exitCode );
}
};
function get_error_msg( ex ) {
var msg = '', withStack = false, isError = false;
switch( ex.fluenterror ) {
case HACKMYSTATUS.themeNotFound:
msg = formatWarning(
chalk.bold("Couldn't find the '" + ex.data + "' theme."),
" Please specify the name of a preinstalled FRESH theme " +
"or the path to a locally installed FRESH or JSON Resume theme.");
break;
case HACKMYSTATUS.copyCSS:
msg = formatWarning("Couldn't copy CSS file to destination folder.");
break;
case HACKMYSTATUS.resumeNotFound:
msg = formatWarning('Please ' + chalk.bold('feed me a resume') +
' in FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.missingCommand:
msg = formatWarning("Please " +chalk.bold("give me a command") + " (");
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, '../use.txt'), 'utf8' ));
break;
case HACKMYSTATUS.invalidCommand:
msg = formatWarning('Invalid command: "'+chalk.bold(ex.attempted)+'"');
break;
case HACKMYSTATUS.resumeNotFoundAlt:
msg = formatWarning('Please ' + chalk.bold('feed me a resume') +
' in either FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.inputOutputParity:
msg = formatWarning('Please ' +
chalk.bold('specify an output file name') +
' for every input file you wish to convert.');
break;
case HACKMYSTATUS.createNameMissing:
msg = formatWarning('Please ' +
chalk.bold('specify the filename of the resume') + ' to create.');
break;
case HACKMYSTATUS.pdfGeneration:
msg = formatError(chalk.bold('ERROR: PDF generation failed. ') +
'Make sure wkhtmltopdf is installed and accessible from your path.');
if( ex.inner ) msg += chalk.red('\n' + ex.inner);
withStack = true;
break;
case HACKMYSTATUS.invalid:
msg = formatError('Validation failed and the --assert option was ' +
'specified.');
break;
case HACKMYSTATUS.invalidFormat:
ex.data.forEach(function(d){ msg +=
formatWarning('The ' + chalk.bold(ex.theme.name.toUpperCase()) +
" theme doesn't support the " + chalk.bold(d.format.toUpperCase()) +
" format.\n");
});
break;
case HACKMYSTATUS.notOnPath:
msg = formatError( ex.engine + " wasn't found on your system path or" +
" is inaccessible. PDF not generated." );
break;
}
return {
msg: msg,
withStack: withStack
};
}
function formatError( msg ) {
return chalk.red.bold( 'ERROR: ' + msg );
}
function formatWarning( brief, msg ) {
return chalk.yellow(brief) + chalk.yellow(msg || '');
}
}());

View File

@ -0,0 +1,35 @@
###
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

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

@ -1,84 +0,0 @@
/**
The HackMyResume date representation.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module fluent-date.js
*/
var moment = require('moment');
/**
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
*/
function FluentDate( dt ) {
this.rep = this.fmt( dt );
}
FluentDate/*.prototype*/.fmt = function( dt ) {
if( (typeof dt === 'string' || 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"
var parts = dt.split(' ');
var month = (months[parts[0]] || abbr[parts[0]]);
var temp = parts[1] + '-' + (month < 10 ? '0' + month : month.toString());
return moment( temp, 'YYYY-MM' );
}
else if( /^\d{4}-\d{1,2}$/.test(dt) ) { // "2015-03", "1998-4"
return moment( dt, 'YYYY-MM' );
}
else if( /^\s*\d{4}\s*$/.test(dt) ) { // "2015"
return moment( dt, 'YYYY' );
}
else if( /^\s*$/.test(dt) ) { // "", " "
var defTime = {
isNull: true,
isBefore: function( other ) {
return( other && !other.isNull ) ? true : false;
},
isAfter: function( other ) {
return( other && !other.isNull ) ? false : false;
},
unix: function() { return 0; },
format: function() { return ''; },
diff: function() { return 0; }
};
return defTime;
}
else {
var mt = moment( dt );
if(mt.isValid())
return mt;
throw 'Invalid date format encountered.';
}
}
else {
if( !dt ) {
return moment();
}
else if( dt.isValid && dt.isValid() )
return dt;
throw 'Unknown date object encountered.';
}
};
var months = {}, abbr = {};
moment.months().forEach(function(m,idx){months[m.toLowerCase()]=idx+1;});
moment.monthsShort().forEach(function(m,idx){abbr[m.toLowerCase()]=idx+1;});
abbr.sept = 9;
module.exports = FluentDate;

View File

@ -0,0 +1,408 @@
###*
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 FreshResume from file. ###
open: ( file, opts ) ->
raw = FS.readFileSync file, 'utf8'
ret = this.parse raw, opts
@imp.file = file
ret
###* 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' )
###*
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}$/
#

View File

@ -1,539 +0,0 @@
/**
Definition of the FRESHResume class.
@license MIT. See LICENSE .md for details.
@module fresh-resume.js
*/
(function() {
var FS = require('fs')
, extend = require('../utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, __ = require('lodash')
, PATH = require('path')
, moment = require('moment')
, XML = require('xml-escape')
, MD = require('marked')
, CONVERTER = require('./convert')
, JRSResume = require('./jrs-resume');
/**
A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
object is an instantiation of that JSON decorated with utility methods.
@class FreshResume
*/
function FreshResume() {
}
/**
Initialize the FreshResume from file.
*/
FreshResume.prototype.open = function( file, title ) {
this.imp = { fileName: file };
this.imp.raw = FS.readFileSync( file, 'utf8' );
return this.parse( this.imp.raw, title );
};
/**
Initialize the the FreshResume from string.
*/
FreshResume.prototype.parse = function( stringData, opts ) {
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.
*/
FreshResume.prototype.parseJSON = function( rep, opts ) {
// Convert JSON Resume to FRESH if necessary
if( rep.basics ) {
rep = CONVERTER.toFRESH( rep );
rep.imp = rep.imp || { };
rep.imp.orgFormat = 'JRS';
}
// Now apply the resume representation onto this object
extend( true, this, rep );
// Set up metadata
opts = opts || { };
if( opts.imp === undefined || opts.imp ) {
this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.name;
}
// Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this );
(opts.sort === undefined || opts.sort) && this.sort();
(opts.compute === undefined || opts.compute) && (this.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
return this;
};
/**
Save the sheet to disk (for environments that have disk access).
*/
FreshResume.prototype.save = function( filename ) {
this.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
return this;
};
/**
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
FreshResume.prototype.saveAs = function( filename, format ) {
if( format !== 'JRS' ) {
this.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
}
else {
var newRep = CONVERTER.toJRS( this );
FS.writeFileSync( filename, JRSResume.stringify( newRep ), 'utf8' );
}
return this;
};
/**
Duplicate this FreshResume instance.
*/
FreshResume.prototype.dupe = function() {
var rnew = new FreshResume();
rnew.parse( this.stringify(), { } );
return rnew;
};
/**
Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
along the way.
*/
FreshResume.stringify = function( obj ) {
function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
return JSON.stringify( obj, replacer, 2 );
};
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way.
*/
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).
*/
FreshResume.prototype.transformStrings = function( filt, transformer ) {
var that = this;
var ret = this.dupe();
// TODO: refactor recursion
function transformStringsInObject( obj, filters ) {
if( !obj ) return;
if( moment.isMoment( obj ) ) return;
if( _.isArray( obj ) ) {
obj.forEach( function(elem, idx, ar) {
if( typeof elem === 'string' || elem instanceof String )
ar[idx] = transformer( null, elem );
else if (_.isObject(elem))
transformStringsInObject( elem, filters );
});
}
else if (_.isObject( obj )) {
Object.keys( obj ).forEach(function(k) {
if( filters.length && _.contains(filters, k) )
return;
var sub = obj[k];
if( typeof sub === 'string' || sub instanceof String ) {
obj[k] = transformer( k, sub );
}
else if (_.isObject( sub ))
transformStringsInObject( sub, filters );
});
}
}
Object.keys( ret ).forEach(function(member){
if( !filt || !filt.length || !_.contains(filt, member) )
transformStringsInObject( ret[ member ], filt || [] );
});
return ret;
};
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
FreshResume.prototype.markdownify = function() {
function MDIN( txt ){
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
function trx(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() {
function trx(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() {
this.imp = (this.imp || { });
return this.imp;
};
/**
Return a unique list of all keywords across all skills.
*/
FreshResume.prototype.keywords = function() {
var 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 === undefined) && true) || clearMeta;
clearMeta && (delete this.imp);
delete this.computed; // Don't use Object.keys() here
delete this.employment;
delete this.service;
delete this.education;
delete this.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.
*/
FreshResume.prototype.count = function( obj ) {
if( !obj ) return 0;
if( obj.history ) return obj.history.length;
if( obj.sets ) return obj.sets.length;
return obj.length || 0;
};
/**
Get the default (starter) sheet.
*/
FreshResume.default = function() {
return new FreshResume().parseJSON( require('fresh-resume-starter') );
};
/**
Add work experience to the sheet.
*/
FreshResume.prototype.add = function( moniker ) {
var defSheet = FreshResume.default();
var newObject = defSheet[moniker].history ?
$.extend( true, {}, defSheet[ moniker ].history[0] ) :
(moniker === 'skills' ?
$.extend( true, {}, defSheet.skills.sets[0] ) :
$.extend( true, {}, defSheet[ moniker ][0] ));
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 schemaObj = require('fresca');
var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // See Note [1].
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
var ret = validate( this );
if( !ret ) {
this.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
}
return ret;
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
FreshResume.prototype.duration = function(unit) {
unit = unit || 'years';
var empHist = __.get(this, 'employment.history');
if( empHist && empHist.length ) {
var firstJob = _.last( empHist );
var careerStart = firstJob.start ? firstJob.safe.start : '';
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
!careerStart.trim())
return 0;
var careerLast = _.max( empHist, function( w ) {
return( w.safe && w.safe.end ) ? w.safe.end.unix() : moment().unix();
});
return careerLast.safe.end.diff( careerStart, unit );
}
return 0;
};
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
FreshResume.prototype.sort = function( ) {
function byDateDesc(a,b) {
return( a.safe.start.isBefore(b.safe.start) ) ? 1
: ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0;
}
function sortSection( key ) {
var ar = __.get(this, key);
if( ar && ar.length ) {
var datedThings = obj.filter( function(o) { return o.start; } );
datedThings.sort( byDateDesc );
}
}
sortSection( 'employment.history' );
sortSection( 'education.history' );
sortSection( 'service.history' );
sortSection( 'projects.history' );
// this.awards && this.awards.sort( function(a, b) {
// return( a.safeDate.isBefore(b.safeDate) ) ? 1
// : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
// });
this.writing && this.writing.sort( function(a, b) {
return( a.safe.date.isBefore(b.safe.date) ) ? 1
: ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0;
});
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
function _parseDates() {
var _fmt = require('./fluent-date').fmt;
var that = this;
// TODO: refactor recursion
function replaceDatesInObject( obj ) {
if( !obj ) return;
if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
obj.forEach(function(elem){
replaceDatesInObject( elem );
});
}
else if (typeof obj === 'object') {
if( obj._isAMomentObject || obj.safe )
return;
Object.keys( obj ).forEach(function(key) {
replaceDatesInObject( obj[key] );
});
['start','end','date'].forEach( function(val) {
if( (obj[val] !== undefined) && (!obj.safe || !obj.safe[val] )) {
obj.safe = obj.safe || { };
obj.safe[ val ] = _fmt( obj[val] );
if( obj[val] && (val === 'start') && !obj.end ) {
obj.safe.end = _fmt('current');
}
}
});
}
}
Object.keys( this ).forEach(function(member){
replaceDatesInObject( that[ member ] );
});
}
/**
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}$/
//

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

@ -0,0 +1,277 @@
###*
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'
###
The FRESHTheme class is a representation of a FRESH theme
asset. See also: JRSTheme.
@class FRESHTheme
###
class FRESHTheme
###
Open and parse the specified theme.
###
open: ( themeFolder ) ->
this.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 )
# Check for an explicit "formats" entry in the theme JSON. If it has one,
# then this theme declares its files explicitly.
if !!@formats
formatsHash = loadExplicit.call this, formatsHash
@explicit = true;
else
formatsHash = loadImplicit.call this, 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 the theme implicitly, by scanning the theme folder for files. TODO:
Refactor duplicated code with loadExplicit. ###
loadImplicit = (formatsHash) ->
# Set up a hash of formats supported by this theme.
that = @
major = false
# Establish the base theme folder
tplFolder = PATH.join @folder, 'src'
# Iterate over all files in the theme folder, producing an array, fmts,
# containing info for each file. While we're doing that, also build up
# the formatsHash object.
fmts = READFILES(tplFolder).map (absPath) ->
# If this file lives in a specific format folder within the theme,
# such as "/latex" or "/html", then that format is the output format
# for all files within the folder.
pathInfo = parsePath absPath
outFmt = ''
isMajor = false
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]
else
that.partials = that.partials || []
that.partials.push( { name: pathInfo.name, path: absPath } )
return null
# Otherwise, the output format is inferred from the filename, as in
# compact-[outputformat].[extension], for ex, compact-pdf.html.
if !outFmt
idx = pathInfo.name.lastIndexOf '-'
outFmt = if idx == -1 then pathInfo.name else pathInfo.name.substr( idx + 1 )
isMajor = true
# We should have a valid output format now.
formatsHash[ outFmt ] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
}
# Create the file representation object.
obj =
action: 'transform'
path: absPath
major: isMajor
orgPath: PATH.relative(tplFolder, absPath)
ext: pathInfo.extname.slice(1)
title: friendlyName( outFmt )
pre: outFmt
# outFormat: outFmt || pathInfo.name,
data: FS.readFileSync( absPath, 'utf8' )
css: null
# Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj )
obj
# 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 the theme explicitly, by following the 'formats' hash
in the theme's JSON settings file.
###
loadExplicit = (formatsHash) ->
# Housekeeping
tplFolder = PATH.join this.folder, 'src'
act = null
that = this
# 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) ->
act = null
# If this file is mentioned in the theme's JSON file under "transforms"
pathInfo = parsePath(absPath)
absPathSafe = absPath.trim().toLowerCase()
outFmt = _.find Object.keys( that.formats ), ( fmtKey ) ->
fmtVal = that.formats[ fmtKey ]
_.some fmtVal.transform, (fpath) ->
absPathB = PATH.join( that.folder, fpath ).trim().toLowerCase()
absPathB == absPathSafe
act = 'transform' if outFmt
# If this file lives in a specific format folder within the theme,
# such as "/latex" or "/html", then that format is the output format
# for all files within the folder.
if !outFmt
portion = pathInfo.dirname.replace tplFolder,''
if portion && portion.trim()
reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig
res = reg.exec portion
res && (outFmt = res[1])
# Otherwise, the output format is inferred from the filename, as in
# compact-[outputformat].[extension], for ex, compact-pdf.html.
if !outFmt
idx = pathInfo.name.lastIndexOf '-'
outFmt = if (idx == -1) then pathInfo.name else pathInfo.name.substr(idx + 1)
# We should have a valid output format now.
formatsHash[ outFmt ] =
formatsHash[ outFmt ] || {
outFormat: outFmt,
files: [],
symLinks: that.formats[ outFmt ].symLinks
};
# Create the file representation object.
obj =
action: act
orgPath: PATH.relative(that.folder, absPath)
path: absPath
ext: pathInfo.extname.slice(1)
title: friendlyName( outFmt )
pre: outFmt
# outFormat: outFmt || pathInfo.name,
data: FS.readFileSync( absPath, 'utf8' )
css: null
# Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj )
obj
# Now, get all the CSS files...
@cssFiles = fmts.filter ( fmt ) -> fmt.ext == 'css'
# For each CSS file, get its corresponding HTML file
@cssFiles.forEach ( cssf ) ->
# For each CSS file, get its corresponding HTML file
idx = _.findIndex fmts, ( fmt ) ->
fmt.pre == cssf.pre && fmt.ext == 'html'
fmts[ idx ].css = cssf.data;
fmts[ idx ].cssPath = cssf.path;
# Remove CSS files from the formats array
fmts = fmts.filter ( fmt) -> fmt.ext != 'css'
formatsHash
###
Return a more friendly name for certain formats.
TODO: Refactor
###
friendlyName = ( val ) ->
val = val.trim().toLowerCase()
friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }
friendly[val] || val
module.exports = FRESHTheme

View File

@ -1,336 +0,0 @@
/**
Definition of the FRESHTheme class.
@module fresh-theme.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var FS = require('fs')
, extend = require('../utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, PATH = require('path')
, parsePath = require('parse-filepath')
, pathExists = require('path-exists').sync
, EXTEND = require('../utils/extend')
, moment = require('moment')
, READFILES = require('recursive-readdir-sync');
/**
The FRESHTheme class is a representation of a FRESH theme
asset. See also: JRSTheme.
@class FRESHTheme
*/
function FRESHTheme() {
}
/**
Open and parse the specified theme.
*/
FRESHTheme.prototype.open = function( themeFolder ) {
this.folder = themeFolder;
// Open the [theme-name].json file; should have the same name as folder
var pathInfo = parsePath( themeFolder );
// Set up a formats hash for the theme
var formatsHash = { };
// Load the theme
var themeFile = PATH.join( themeFolder, 'theme.json' );
var themeInfo = JSON.parse( FS.readFileSync( themeFile, 'utf8' ) );
var that = this;
// Move properties from the theme JSON file to the theme object
EXTEND( true, this, themeInfo );
// Check for an "inherits" entry in the theme JSON.
if( this.inherits ) {
var cached = { };
_.each( this.inherits, function(th, key) {
var themesFolder = require.resolve('fresh-themes');
var d = parsePath( themeFolder ).dirname;
var themePath = PATH.join(d, th);
cached[ th ] = cached[th] || new FRESHTheme().open( themePath );
formatsHash[ key ] = cached[ th ].getFormat( key );
});
}
// Check for an explicit "formats" entry in the theme JSON. If it has one,
// then this theme declares its files explicitly.
if( !!this.formats ) {
formatsHash = loadExplicit.call( this, formatsHash );
this.explicit = true;
}
else {
formatsHash = loadImplicit.call( this, formatsHash );
}
// Cache
this.formats = formatsHash;
// Set the official theme name
this.name = parsePath( this.folder ).name;
return this;
};
/**
Determine if the theme supports the specified output format.
*/
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 ];
};
/**
Load the theme implicitly, by scanning the theme folder for
files. TODO: Refactor duplicated code with loadExplicit.
*/
function loadImplicit(formatsHash) {
// Set up a hash of formats supported by this theme.
var that = this;
var major = false;
// Establish the base theme folder
var tplFolder = PATH.join( this.folder, 'src' );
// Iterate over all files in the theme folder, producing an array, fmts,
// containing info for each file. While we're doing that, also build up
// the formatsHash object.
var fmts = READFILES(tplFolder).map( function(absPath) {
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
var pathInfo = parsePath(absPath);
var outFmt = '', isMajor = false;
var portion = pathInfo.dirname.replace(tplFolder,'');
if( portion && portion.trim() ) {
if( portion[1] === '_' ) return;
var reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
var res = reg.exec( portion );
if( res ) {
if( res[1] !== 'partials' ) {
outFmt = res[1];
}
else {
that.partials = that.partials || [];
that.partials.push( { name: pathInfo.name, path: absPath } );
return null;
}
}
}
// Otherwise, the output format is inferred from the filename, as in
// compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-');
outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1);
isMajor = true;
}
// We should have a valid output format now.
formatsHash[ outFmt ] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
};
// Create the file representation object.
var obj = {
action: 'transform',
path: absPath,
major: isMajor,
orgPath: PATH.relative(tplFolder, absPath),
ext: pathInfo.extname.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
data: FS.readFileSync( absPath, 'utf8' ),
css: null
};
// Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj );
return obj;
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){
return fmt && (fmt.ext === 'css');
}))
// 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.
.forEach(function( cssf ) {
var idx = _.findIndex(fmts, function( fmt ) {
return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
});
cssf.action = null;
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 };
}
}
});
// Remove CSS files from the formats array
fmts = fmts.filter( function( fmt) {
return fmt && (fmt.ext !== 'css');
});
return formatsHash;
}
/**
Load the theme explicitly, by following the 'formats' hash
in the theme's JSON settings file.
*/
function loadExplicit(formatsHash) {
// Housekeeping
var tplFolder = PATH.join( this.folder, 'src' );
var act = null;
var that = this;
// Iterate over all files in the theme folder, producing an array, fmts,
// containing info for each file. While we're doing that, also build up
// the formatsHash object.
var fmts = READFILES( tplFolder ).map( function( absPath ) {
act = null;
// If this file is mentioned in the theme's JSON file under "transforms"
var pathInfo = parsePath(absPath);
var absPathSafe = absPath.trim().toLowerCase();
var outFmt = _.find(
Object.keys( that.formats ),
function( fmtKey ) {
var fmtVal = that.formats[ fmtKey ];
return _.some( fmtVal.transform, function(fpath) {
var absPathB = PATH.join( that.folder, fpath )
.trim().toLowerCase();
return absPathB === absPathSafe;
});
});
if( outFmt ) {
act = 'transform';
}
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
if( !outFmt ) {
var portion = pathInfo.dirname.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
var res = reg.exec( portion );
res && (outFmt = res[1]);
}
}
// Otherwise, the output format is inferred from the filename, as in
// compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-');
outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1);
}
// We should have a valid output format now.
formatsHash[ outFmt ] =
formatsHash[ outFmt ] || {
outFormat: outFmt,
files: [],
symLinks: that.formats[ outFmt ].symLinks
};
// Create the file representation object.
var obj = {
action: act,
orgPath: PATH.relative(that.folder, absPath),
path: absPath,
ext: pathInfo.extname.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
data: FS.readFileSync( absPath, 'utf8' ),
css: null
};
// Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj );
return obj;
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){
return fmt.ext === 'css';
}))
// For each CSS file, get its corresponding HTML file
.forEach(function( cssf ) {
// For each CSS file, get its corresponding HTML file
var idx = _.findIndex(fmts, function( fmt ) {
return fmt.pre === cssf.pre && fmt.ext === 'html';
});
fmts[ idx ].css = cssf.data;
fmts[ idx ].cssPath = cssf.path;
});
// Remove CSS files from the formats array
fmts = fmts.filter( function( fmt) {
return fmt.ext !== 'css';
});
return formatsHash;
}
/**
Return a more friendly name for certain formats.
TODO: Refactor
*/
function friendlyName( val ) {
val = val.trim().toLowerCase();
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val;
}
module.exports = FRESHTheme;
}());

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

@ -0,0 +1,343 @@
###*
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 JSResume from file. ###
open: ( file, opts ) ->
raw = FS.readFileSync file, 'utf8'
ret = this.parse raw, opts
@imp.file = file
ret
###* 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().open PATH.join( __dirname, 'empty-jrs.json'), 'Empty'
###*
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

View File

@ -1,432 +0,0 @@
/**
Definition of the JRSResume class.
@license MIT. See LICENSE.md for details.
@module jrs-resume.js
*/
(function() {
var FS = require('fs')
, extend = require('../utils/extend')
, validator = require('is-my-json-valid')
, _ = require('underscore')
, PATH = require('path')
, MD = require('marked')
, CONVERTER = require('./convert')
, moment = require('moment');
/**
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
*/
function JRSResume() {
}
/**
Initialize the JSResume from file.
*/
JRSResume.prototype.open = function( file, title ) {
//this.imp = { fileName: file }; <-- schema violation, tuck it into .basics
this.basics = {
imp: {
fileName: file,
raw: FS.readFileSync( file, 'utf8' )
}
};
return this.parse( this.basics.imp.raw, title );
};
/**
Initialize the the JSResume from string.
*/
JRSResume.prototype.parse = function( stringData, opts ) {
opts = opts || { };
var rep = JSON.parse( stringData );
return this.parseJSON( rep, opts );
};
/**
Initialize the JRSResume 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.
*/
JRSResume.prototype.parseJSON = function( rep, opts ) {
opts = opts || { };
extend( true, this, rep );
// Set up metadata
if( opts.imp === undefined || opts.imp ) {
this.basics.imp = this.basics.imp || { };
this.basics.imp.title =
(opts.title || this.basics.imp.title) || this.basics.name;
this.basics.imp.orgFormat = 'JRS';
}
// Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this );
(opts.sort === undefined || opts.sort) && this.sort();
(opts.compute === undefined || opts.compute) && (this.basics.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
return this;
};
/**
Save the sheet to disk (for environments that have disk access).
*/
JRSResume.prototype.save = function( filename ) {
this.basics.imp.fileName = filename || this.basics.imp.fileName;
FS.writeFileSync(this.basics.imp.fileName, this.stringify( this ), 'utf8');
return this;
};
/**
Save the sheet to disk in a specific format, either FRESH or JRS.
*/
JRSResume.prototype.saveAs = function( filename, format ) {
if( format === 'JRS' ) {
this.basics.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.basics.imp.fileName, this.stringify(), 'utf8' );
}
else {
var newRep = CONVERTER.toFRESH( this );
var stringRep = CONVERTER.toSTRING( newRep );
FS.writeFileSync( filename, stringRep, 'utf8' );
}
return this;
};
/**
Return the resume format.
*/
JRSResume.prototype.format = function() {
return 'JRS';
};
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
JRSResume.stringify = function( obj ) {
function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
return JSON.stringify( obj, replacer, 2 );
};
JRSResume.prototype.stringify = function() {
return JRSResume.stringify( this );
};
/**
Return a unique list of all keywords across all skills.
*/
JRSResume.prototype.keywords = function() {
var flatSkills = [];
if( this.skills && this.skills.length ) {
this.skills.forEach( function( s ) {
flatSkills = _.union( flatSkills, s.keywords );
});
}
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() {
this.basics = this.basics || { imp: { } };
return this.basics;
};
/**
Reset the sheet to an empty state.
*/
JRSResume.prototype.clear = function( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
clearMeta && (delete this.imp);
delete this.basics.computed; // Don't use Object.keys() here
delete this.work;
delete this.volunteer;
delete this.education;
delete this.awards;
delete this.publications;
delete this.interests;
delete this.skills;
delete this.basics.profiles;
};
/**
Get the default (empty) sheet.
*/
JRSResume.default = function() {
return new JRSResume().open(
PATH.join( __dirname, 'empty-jrs.json'), 'Empty'
);
};
/**
Add work experience to the sheet.
*/
JRSResume.prototype.add = function( moniker ) {
var defSheet = JRSResume.default();
var newObject = $.extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject );
return newObject;
};
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
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( ) { // TODO: ↓ fix this path ↓
var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ),'utf8');
var schemaObj = JSON.parse( schema );
var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
var ret = validate( this );
if( !ret ) {
this.basics.imp = this.basics.imp || { };
this.basics.imp.validationErrors = validate.errors;
}
return ret;
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
JRSResume.prototype.duration = function( unit ) {
unit = unit || 'years';
if( this.work && this.work.length ) {
var careerStart = this.work[ this.work.length - 1].safeStartDate;
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
!careerStart.trim())
return 0;
var careerLast = _.max( this.work, function( w ) {
return w.safeEndDate.unix();
}).safeEndDate;
return careerLast.diff( careerStart, unit );
}
return 0;
};
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
JRSResume.prototype.sort = function( ) {
this.work && this.work.sort( byDateDesc );
this.education && this.education.sort( byDateDesc );
this.volunteer && this.volunteer.sort( byDateDesc );
this.awards && this.awards.sort( function(a, b) {
return( a.safeDate.isBefore(b.safeDate) ) ? 1
: ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0;
});
this.publications && this.publications.sort( function(a, b) {
return( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) ? 1
: ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0;
});
function byDateDesc(a,b) {
return( a.safeStartDate.isBefore(b.safeStartDate) ) ? 1
: ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0;
}
};
JRSResume.prototype.dupe = function() {
var 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 that = this;
var ret = this.dupe();
function HD(txt) {
return '@@@@~' + txt + '~@@@@';
}
function HDIN(txt){
//return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
return HD(txt);
}
// TODO: refactor recursion
function hardenStringsInObject( obj, inline ) {
if( !obj ) return;
inline = inline === undefined || inline;
if( Object.prototype.toString.call( obj ) === '[object Array]' ) {
obj.forEach(function(elem, idx, ar){
if( typeof elem === 'string' || elem instanceof String )
ar[idx] = inline ? HDIN(elem) : HD( elem );
else
hardenStringsInObject( elem );
});
}
else if (typeof obj === 'object') {
Object.keys( obj ).forEach(function(key) {
var sub = obj[key];
if( typeof sub === 'string' || sub instanceof String ) {
if( _.contains(['skills','url','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] = inline ? HDIN( obj[key] ) : HD( obj[key] );
}
else
hardenStringsInObject( sub );
});
}
}
Object.keys( ret ).forEach(function(member){
hardenStringsInObject( ret[ member ] );
});
return ret;
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
function _parseDates() {
var _fmt = require('./fluent-date').fmt;
this.work && this.work.forEach( function(job) {
job.safeStartDate = _fmt( job.startDate );
job.safeEndDate = _fmt( job.endDate );
});
this.education && this.education.forEach( function(edu) {
edu.safeStartDate = _fmt( edu.startDate );
edu.safeEndDate = _fmt( edu.endDate );
});
this.volunteer && this.volunteer.forEach( function(vol) {
vol.safeStartDate = _fmt( vol.startDate );
vol.safeEndDate = _fmt( vol.endDate );
});
this.awards && this.awards.forEach( function(awd) {
awd.safeDate = _fmt( awd.date );
});
this.publications && this.publications.forEach( function(pub) {
pub.safeReleaseDate = _fmt( pub.releaseDate );
});
}
/**
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,
major: true,
ext: 'html',
css: null
}]
pdf:
outFormat: 'pdf'
files: [{
action: 'transform',
render: this.render,
major: 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;

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