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

Compare commits

...

215 Commits

Author SHA1 Message Date
af896c85ea Bump version to 1.4.2. 2016-01-07 02:06:55 -05:00
6a7bb5ea5b Update README. 2016-01-07 01:09:48 -05:00
3b6f2ad37e Introduce FAQ.
Use a separate Markdown document instead of the GH wiki so that the FAQ
is present after clone and advertises itself in the root folder.
2016-01-07 00:58:40 -05:00
101eebdd95 Update tests. 2016-01-06 14:17:27 -05:00
830c36818e Tweak missing file message for "new" command. 2016-01-06 14:15:27 -05:00
39e995213f Improve starter resume.
"hackmyresume new" should emit a starter resume that a) has example
information and b) validates.
2016-01-06 14:09:22 -05:00
37a053722d Update Travis URLs. 2016-01-06 11:36:40 -05:00
12fcf3b0cb Fix package.json glitch. 2016-01-06 11:28:09 -05:00
43ad9c1c71 Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-06 11:24:02 -05:00
4f9207a868 Fix: formatDate helper references the moment method, not the momentDate object 2016-01-06 11:23:39 -05:00
3d1f589bc1 formatDate helper now will only use moment if date is valid. If it's not, will use the user inputted value or a fallback parameter, if it is provided 2016-01-06 11:23:38 -05:00
ae436a3b84 Scrub. 2016-01-06 11:18:50 -05:00
202bb44c76 Update contributors.
@robertmain @jjanusch
2016-01-06 11:18:31 -05:00
041c609ff0 Merge pull request #85 from jjanusch/feat/present-formatter
formatDate template helper error handling and fallback
2016-01-06 11:00:17 -05:00
712b504168 Support global theme partials (interim). 2016-01-06 10:48:51 -05:00
bc9f0d468f Update tests w/ new validation behavior. 2016-01-06 00:44:46 -05:00
2d20077c08 Support --assert option for validate command.
Cause HMR to return an error code if validation fails and the --assert
option is present.
2016-01-06 00:44:34 -05:00
f61deda4e8 Fix format detection error in validate logic. 2016-01-06 00:21:18 -05:00
8203fa50ae Prep convert.js. 2016-01-06 00:20:30 -05:00
c5eab0fd9c Scrub. 2016-01-05 23:59:41 -05:00
40e71238ac Scrub. 2016-01-05 23:46:01 -05:00
9d75b207d1 Formalize empty-fresh.json dependency. 2016-01-05 23:28:49 -05:00
9b52c396d3 Fix missing method rename. 2016-01-05 22:32:46 -05:00
2759727984 Add convenience method. 2016-01-05 22:26:16 -05:00
e230d640cb Rename imp() to i() (interim). 2016-01-05 22:02:11 -05:00
d69688697c Update README. 2016-01-05 19:48:11 -05:00
9f7ec62b18 Bump fresh-themes to 0.11.0-beta. 2016-01-05 10:26:29 -05:00
b1a02918ff Support --no-tips flag. 2016-01-05 10:10:24 -05:00
ec05f6737a Emit JSON Resume theme instructions. 2016-01-05 10:10:12 -05:00
da5db6477b Introduce --color and --no-color options.
These are handled by Chalk, but need to be registered with Commander.js
in order for Chalk to see them.
2016-01-05 09:42:39 -05:00
0f580efb2b Mention ANALYZE command in man page. 2016-01-05 09:38:42 -05:00
ff23ee508b Restore app title. 2016-01-05 09:38:21 -05:00
2819faeb6f Improve theme/format inheritance (interim). 2016-01-05 09:28:40 -05:00
d205e882f6 Introduce FRESH theme/format inheritance.
Support "inherits" property in theme.json (FRESH themes only).
2016-01-05 06:34:56 -05:00
3f40441b0a Bump FRESCA version to 0.3.0. 2016-01-05 05:09:29 -05:00
6185f20ec9 Sort project history by default. 2016-01-05 05:00:04 -05:00
6a61989eb4 Introduce {{dateRange}} and {{camelCase}} helpers. 2016-01-05 04:59:51 -05:00
d658a069cd Rename {{hasSection}} helper to {{section}}. 2016-01-05 04:59:26 -05:00
25688dbe8e Bump fresh-test-resume to 0.2.1. 2016-01-05 01:41:04 -05:00
98362b9687 Bump fresh-test-resumes version. 2016-01-05 00:04:10 -05:00
4c31c96891 Introduce has/hasSection helpers. 2016-01-05 00:03:54 -05:00
219209c6ca Fix logic glitch in {{sectionTitle}} helper. 2016-01-04 19:46:45 -05:00
eff9fc51cb Integrate fresh-test-resumes module. 2016-01-04 19:45:49 -05:00
2ba23ee80d Add support for user-definable section titles.
Introduce a {{sectionTitle}} helper; requires theme updates.
2016-01-04 16:20:48 -05:00
0f83f8f5c2 Merge remote-tracking branch 'refs/remotes/origin/master' into dev 2016-01-04 16:13:37 -05:00
4ba3a3f2a9 Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-04 08:09:12 -05:00
db486a48aa Update README. 2016-01-04 08:06:51 -05:00
2cab1195e8 Fix 'create' alias. 2016-01-04 07:25:48 -05:00
ce75f09210 Refactor API interface. 2016-01-04 07:23:20 -05:00
a8fed1b69b Add missing semicolon. 2016-01-04 04:15:13 -05:00
62ca2020d8 Bump FRESH themes version. 2016-01-04 04:15:01 -05:00
f65cf8880e Add support for external options file. 2016-01-04 02:50:00 -05:00
c8d4a3deb3 Handle global options.
Fix broken --silent flag and set up -o/-opts.
2016-01-04 01:49:35 -05:00
d5e2a45034 Output theme message on generate. 2016-01-04 00:58:41 -05:00
2465f2ce1c Fix gap analysis glitches. 2016-01-04 00:14:43 -05:00
f2001bcbb1 Add a couple baseline "analyze" tests. 2016-01-03 23:18:35 -05:00
d5afb3eb2e Handle missing dates during gap inspection. 2016-01-03 23:17:36 -05:00
c711cb7922 Improve sorting. 2016-01-03 23:17:18 -05:00
e45e0316f6 Remove extraneous regex. 2016-01-03 10:07:58 -05:00
08ab512f4c Add overlap analysis. 2016-01-03 09:48:43 -05:00
f2bf09bf96 Allow variable-unit resume duration. 2016-01-03 09:48:22 -05:00
75e2b1c131 Improve keyword acquisition. 2016-01-03 09:48:02 -05:00
0b7ef16a41 Improve accuracy of keyword counts. 2016-01-03 07:36:05 -05:00
247eec396c Fix string iteration filtering glitch. 2016-01-03 07:35:47 -05:00
46c7fa9838 Add baseline keyword analysis. 2016-01-03 06:39:46 -05:00
b3fb2c7130 Scrub. 2016-01-03 05:06:54 -05:00
c3ec3f28bd Introduce section totals inspector. 2016-01-03 05:03:31 -05:00
0a8ee721e8 Allow for multiple PDF engines / support Phantom PDFs.
Start formalizing PDF generation apparatus and support a `--pdf`
parameter allowing the user to specify the flavor of PDF generation.
2016-01-03 04:11:42 -05:00
8d7cf32988 Finish Commander.js integration. 2016-01-03 03:18:56 -05:00
655ecebaa5 Clean up comments. 2016-01-03 02:40:04 -05:00
8fc0fa99d3 Remove unnecessary indirection. 2016-01-03 02:39:43 -05:00
69e8adc1cc Remove 'minimist' dependency. 2016-01-03 02:25:39 -05:00
6b3396e01b Use Commander.js for invocations. 2016-01-03 02:22:26 -05:00
a95b52acd0 Refactor command processing. 2016-01-02 00:15:46 -05:00
47553b6def Fix ICE encoding issues.
Fix issue where @@@@ is appearing in generated resumes.
2016-01-01 20:27:46 -05:00
e4a549ed30 Tests: Add ICE detection test.
ICE is the internal boilerplate we use to freeze/unfreeze themes when
trying to force-feed them Markdown or other formatted data.
2016-01-01 20:26:47 -05:00
dd2148bb92 Fix: formatDate helper references the moment method, not the momentDate object 2016-01-01 18:05:33 -05:00
d8b9d86896 Scrub. 2016-01-01 17:30:57 -05:00
889bd4bfc5 formatDate helper now will only use moment if date is valid. If it's not, will use the user inputted value or a fallback parameter, if it is provided 2016-01-01 17:27:49 -05:00
13fc903b2b Catch JSON syntax errors for all commands.
...and emit line/column info.
2016-01-01 17:20:42 -05:00
8c8dbfed72 Adjust test paths. 2016-01-01 15:06:36 -05:00
2b669cf35c Tweak error handling for cmd params. 2016-01-01 15:06:16 -05:00
5a2d892b85 Scrub error-handler.js. 2016-01-01 14:59:21 -05:00
37a7c318d5 Remove stack trace for ENOENT. 2016-01-01 14:58:56 -05:00
43873efcab Tweak analyze command error. 2016-01-01 14:38:52 -05:00
bb28e5aa8e Support --help option.
Support standard syntax for the HELP command.
2016-01-01 14:38:00 -05:00
c17261cd25 Merge pull request #81 from tjlav5/master
Fix relative theme directory
2016-01-01 13:46:55 -05:00
49e56cc226 Fix relative theme directory
The theme directory assumes it was a child of the HackMyResume module, but NPM3 will actually flatten this out. Following the same logic that the template-generator uses, find the path to the themes using NPMs require method.
2016-01-01 11:27:05 -05:00
84ad6cf356 Add missing chalk references. 2016-01-01 04:57:50 -05:00
b96526da31 Replace chalk with colors in tests. 2016-01-01 04:48:20 -05:00
cb14452df3 Replace colors with chalk.
Chalk has a few more options and doesn't mess around with
String.prototype.
2016-01-01 04:44:14 -05:00
d54b9a6d6c Remove unused method. 2016-01-01 03:45:14 -05:00
6285c2db3b Introduce "analyze" verb and framework.
Introduce a new "analyze" command and start setting up the inspector /
analyzer pipeline with a simple "gap analysis" inspector using a
reference-counted gap detection approach.
2016-01-01 03:39:48 -05:00
3453293c79 Bump version to 1.3.1. 2015-12-31 20:41:54 -05:00
fb32cb0d78 Tests: Bump Johnny's expected duration to 4 years.
Happy New Year, everybody.
2015-12-31 20:00:39 -05:00
baccb75256 Tests: fix Travis error on Node 0.10.
Node 0.10 doesn't have path.parse, so use require('parse-filepath') as a
workaround.
2015-12-31 19:51:06 -05:00
5c39c1c93d Remove extraneous console.log. 2015-12-31 19:47:55 -05:00
48cc315fc8 Update Travis shields.
Add version and a badge for the /dev branch.
2015-12-31 19:17:56 -05:00
ea8da6811a Include Node 0.10 in Travis tests.
We've already done some work to support legacy Node 0.10 (ex
https://github.com/hacksalot/HackMyResume/issues/31#issuecomment-167155845)
no reason to drop this support by omitting tests.
2015-12-31 18:31:39 -05:00
dbda48c16d Add additional validate tests. 2015-12-31 18:24:45 -05:00
bc710b5c6e Merge pull request #77 from hacksalot/dev
v1.3.0 changes
2015-12-31 06:43:34 -05:00
b85d40b1b3 Improve XML encoding for Word docs.
Fix various encoding errors.
2015-12-31 06:38:30 -05:00
069c02ddcc Interim changes supporting v1.3.0. 2015-12-31 03:34:41 -05:00
1f6d77fc28 Bump version to 1.3.0. 2015-12-31 03:18:02 -05:00
2b4266ee42 Merge pull request #69 from zhuangya/missing-extend-def-fix-68
fix: missing extend method
2015-12-30 22:11:20 -05:00
2b3c83c57e Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2015-12-30 22:03:38 -05:00
6f37ccdee3 Merge pull request #75 from hacksalot/dev
Changes for v1.3.0-beta
2015-12-30 21:26:27 -05:00
df27924ac2 Add Johnny Trouble to tests. 2015-12-30 21:07:28 -05:00
3cf24cfb40 Fix PNG generation glitch. 2015-12-30 20:11:21 -05:00
3acf648eb4 Expose helpers to Underscore engine.
Get the same set of helpers working for Underscore and Handlebars
engines. Needs refactoring.
2015-12-30 20:11:09 -05:00
76cafa4249 Fix reference error in explicit themes. 2015-12-30 20:10:14 -05:00
55943bf49a Fix missing semicolon. 2015-12-30 20:09:39 -05:00
a280d8acb2 Support CSS embedding vs. linking. 2015-12-30 19:45:50 -05:00
558a321fe8 Refactor generator logic. 2015-12-30 18:52:41 -05:00
d901047043 Update fluent-themes --> fresh-themes. 2015-12-30 18:50:58 -05:00
d4e0a0fa05 Add {{styleSheet}} helper (placeholder). 2015-12-30 18:19:00 -05:00
22554c61c5 Rename and bump fluent-themes dependency. 2015-12-30 18:18:11 -05:00
72de1bbd33 Scrub. 2015-12-30 15:21:58 -05:00
2ff912e687 Scrub. 2015-12-30 15:11:18 -05:00
ccadb0416f Move freebie formats out of theme class. 2015-12-30 15:03:26 -05:00
5e51beddf7 Refactor. 2015-12-30 14:48:22 -05:00
97c9ba08d0 Fix: Broken HELP command. 2015-12-30 14:00:09 -05:00
39d61c66b9 Finish Theme --> FreshTheme rename. 2015-12-30 13:22:18 -05:00
7a1eadb3fc Tweak error messages.
Stay away from language like "please specify a valid input resume". The
fluentcv fork can use corporate-speak. HackMyResume is more like a
gremlin -- feed it, but never after midnight.
2015-12-30 13:12:51 -05:00
1bcc2f7d0c Add formal support for aliases.
new/create and build/generate
2015-12-30 13:00:30 -05:00
e3cb949992 Fix: Exception when HMR is run without params. 2015-12-30 12:59:21 -05:00
a0c356941c Remove unnecessary line. 2015-12-30 12:44:16 -05:00
3c7868a750 Scrub. 2015-12-30 12:38:01 -05:00
3e7d9c0411 Integrate JRSTheme class. 2015-12-30 12:37:26 -05:00
b21fd93d66 Introduce JRSTheme class.
Start splitting out logic into dedicated abstractions for both FRESH and
JSON Resume themes given the different structure and use cases of each.
2015-12-30 12:08:46 -05:00
37e75acd86 Merge remote-tracking branch 'refs/remotes/origin/master' into dev 2015-12-30 12:06:02 -05:00
6280a18c14 fix: missing extend method
fix #68
2015-12-30 19:20:22 +08:00
5bc8b9c987 Merge remote-tracking branch 'refs/remotes/origin/dev' 2015-12-29 17:58:41 -05:00
0c570f8512 Update README. 2015-12-29 17:43:27 -05:00
7593afa586 Adjust package.json versions.
Relax to v1.3.0-beta and bump fluent-themes version.
2015-12-29 17:33:16 -05:00
417d07f469 Merge pull request #60 from zhuangya/images
Thanks @zhuangya! Nice work.
2015-12-29 17:21:37 -05:00
b803eba934 Scrub string.js.
Will probably be retired in favor of Node reusables.
2015-12-29 10:26:30 -05:00
483207e5a0 Improve Markdown support for JSON Resume themes. 2015-12-29 10:01:45 -05:00
02ef2b2241 Improve error handling.
Better support for spawn errors encountered during generation (for ex,
PDFs through wkhtml) + general refactoring.
2015-12-29 06:35:55 -05:00
13430bcad5 Refactor status codes. 2015-12-29 05:09:05 -05:00
e65c0e128e Fix tests glitch. 2015-12-29 03:50:00 -05:00
bf5c040971 Copy JRS theme assets to target. 2015-12-29 03:10:26 -05:00
5dd3d1a3b4 chore: remove debugging console logs 2015-12-29 03:40:42 +08:00
6b0ea0c7bd add: png format 2015-12-29 03:29:13 +08:00
6bc6b3262e Add tests for FRESH/JRS cross-generation.
Ability to generate JSON Resume themes from FRESH format resumes and
vice-versa.
2015-12-28 04:39:03 -05:00
3c1ae4cbd1 Add baseline support for local generation of JSON Resume themes. 2015-12-28 04:37:42 -05:00
547b87afc6 LINT prior to running tests. 2015-12-28 04:17:48 -05:00
db31744c98 Adjust "npm test" command.
Fix issue with tests being run twice and run tests through Grunt for
LINTing and other pre/post processing.
2015-12-28 04:16:53 -05:00
9423a19842 Remove extraneous references to "tests" plural. 2015-12-28 04:01:30 -05:00
07b303e530 Bump version to 1.3.0. 2015-12-28 03:51:39 -05:00
ec51148374 Introduce interim contribution guidelines. 2015-12-27 00:08:45 -05:00
0514f7805c Add contributors to package.json.
Contributors first, author last.
2015-12-26 22:47:39 -05:00
dfa19899b0 Merge pull request #41 from zhuangya/npm-test
package.json test scripts and travis :)
2015-12-26 21:12:39 -05:00
1265ecab9f chore: remove generated resume, more node ci
- node 0.11 0.12
- remove and ignore `test/sandbox` from git
2015-12-26 19:13:15 +08:00
1ad297ec7a add: missing travis-image url 2015-12-25 07:08:17 +08:00
68628e3304 add: travis yml and badge 2015-12-25 07:06:52 +08:00
1a6d7d5723 change: package.json enhancement
- add `scripts` for something like `npm test` and `npm run grunt`
- add missing devDep: `grunt-cli`
2015-12-25 07:03:16 +08:00
78a8b9c58e Merge pull request #40 from hacksalot/rel/v1.2.2
rel/v1.2.2
2015-12-24 17:44:56 -05:00
5e7abb66bd Safer source format conversions.
Quick fix against missing fields in FRESH and/or JRS (ahead of introing
more robust standalone converter thing). Address portions of #31 and
#33.
2015-12-24 17:51:26 -05:00
358c397bb9 Show call stack on error.
Hat tip @Furchin.
2015-12-24 16:22:29 -05:00
3d41528059 Fix path parsing issue on prev versions of Node.js.
Work around absence of path.parse in Node versions < v0.12. Addresses
#31 and #33.
2015-12-24 16:18:38 -05:00
79637b611a Bump version to 1.2.2. 2015-12-24 16:09:37 -05:00
5de796b119 Merge pull request #35 from hacksalot/rel/v1.2.1
rel/v1.2.1
2015-12-24 07:41:12 -05:00
bf84341acf Version -> 1.2.1
Holding off on 1.3.0 pending HTTP/HTTPS support. #rebase #semver
2015-12-24 07:42:07 -05:00
bbac1fdceb Improve test coverage around incomplete JRS resumes.
Add quick sanity checks around incomplete or irregular JRS-format
resumes. Also decorate existing JRS test resumes (/tests/resumes/) with
current 0.0.0 JRS version ahead of 1.0.0 release.
2015-12-24 06:08:45 -05:00
c5ee1ee33c Quick fix for ".history" errors.
Affects #31 and #33.
2015-12-24 04:05:56 -05:00
c74eda90ed Introduce lodash dependency. 2015-12-24 04:05:17 -05:00
ef2fe95bd8 Remove unused method. 2015-12-24 04:04:44 -05:00
e2589b3730 Fix validate command error.
Still hitting some inconsistent behavior in different NPM
versions/platforms with invalid uppercase dependency names per
https://github.com/npm/npm/issues/3692. Partial fix for #33.
2015-12-24 03:23:56 -05:00
ebad1677bc Replace file-exists.js with NPM path-exists. 2015-12-22 18:55:17 -05:00
dab6ebfd82 Bump fluent-themes version to 0.8.0-beta. 2015-12-22 18:49:29 -05:00
dd61b5360a Update package.json contact info. 2015-12-22 18:49:11 -05:00
fced92a5a0 Bump version to 1.3.0. 2015-12-22 18:47:23 -05:00
64db1a654e Merge pull request #30 from hacksalot/rel/v1.2.0
rel/v1.2.0
2015-12-21 02:57:59 -05:00
31830ee759 Silence tests. 2015-12-21 03:04:40 -05:00
1c05846a4f Add CLI tests. 2015-12-21 02:58:16 -05:00
1db9c2e420 Fix README glitch. 2015-12-21 02:58:01 -05:00
c966f6766c Refactor verbs to separate files. 2015-12-21 02:56:02 -05:00
65b6359fd8 Bump version to 1.2.0. 2015-12-21 00:52:23 -05:00
a54476eede Reaffirm string-based generation.
In recent commits, HackMyResume generation logic, much like the pilots
in Top Gun who became too reliant on air-to-air missiles and lost the
true art of dogfighting, has become dependent on file-based generation
as implicit file assumptions have crept in. This commit reaffirms the
file-less, string-based nature of the generation process and, as a side
effect, adjusts the behavior of (binary) PDF generation to match.
2015-12-21 00:36:08 -05:00
7c0354046c Merge pull request #29 from hacksalot/final/v1.1.0
final/v1.1.0
2015-12-20 20:46:19 -05:00
43cd1c7e52 Allow TO keyword to be omitted.
If the TO keyword is missing, assume the last file passed in is the
destination file.
2015-12-20 20:53:21 -05:00
f80c333361 Scrub. 2015-12-20 20:22:46 -05:00
cdbb264093 Add string convenience method. 2015-12-20 18:42:02 -05:00
87b3bbe785 Bump version to 1.1.0. 2015-12-20 18:41:51 -05:00
b92cf7298a Refactor helpers.
Helpers shouldn't be specific to a given template engine (eg,
Handlebars) in order to allow sharing of helpers between different
template engines. Isolate abstract helpers in another module and apply
them via Handlebars.registerHelper and as necessary for other template
engines.
2015-12-20 18:24:28 -05:00
93456b5f40 Merge pull request #28 from hacksalot/rel-next
rel-next
2015-12-20 15:56:43 -05:00
72f29bf402 Update README. 2015-12-20 15:59:30 -05:00
f6fc384466 Add keywords to package.json. 2015-12-20 15:43:31 -05:00
c5ab3fdfae Bump version to 1.0.1. 2015-12-20 15:43:20 -05:00
78c5081a29 Support Markdown-driven hyperlinks in MS Word. 2015-12-20 15:42:31 -05:00
d0c181ee8c Merge pull request #26 from hacksalot/rel-v1.0.0
v1.0.0
2015-12-19 12:33:19 -05:00
80c6bb6e8b Rename to HMR. 2015-12-19 12:37:42 -05:00
786b3fd3b2 Merge pull request #25 from fluentdesk/v0.11.0
v0.11.0
2015-12-19 08:58:59 -05:00
f0a22be731 Skip underscore-prefixed folders during theme load. 2015-12-19 08:39:36 -05:00
ade60022fd Introduce new helpers. 2015-12-19 00:24:21 -05:00
7daba910ed Bump fluent-themes version. 2015-12-18 17:27:40 -05:00
a016d6d91a Update README. 2015-12-18 17:23:40 -05:00
fcaa97ed35 Change author info. 2015-12-18 15:35:00 -05:00
bb7373a229 Remove duplicate is-my-json-valid dependency. 2015-12-18 15:34:52 -05:00
759dcc30e7 Update tests. 2015-12-18 15:34:30 -05:00
0e47f02a33 Rename tests/jane-doe.json. 2015-12-18 15:34:00 -05:00
5fe90517e7 Auto-clean tests folder. 2015-12-18 15:33:18 -05:00
92128da381 Install URL-trimming helper. 2015-12-18 14:51:51 -05:00
1441fe3ae5 Class-ify Underscore/Handlebars engine. 2015-12-18 13:17:07 -05:00
b0bc71cd66 Introduce "either" helper for Handlebars themes. 2015-12-18 13:00:47 -05:00
e908e8bb34 Add missing Underscore require(). 2015-12-18 10:13:50 -05:00
d708a6c6d8 Refactor Handlebars helpers. 2015-12-18 10:10:30 -05:00
a630741098 Fix exception. 2015-12-18 10:10:19 -05:00
01d148e47c Bump version to 0.11.0. 2015-12-18 10:08:52 -05:00
dbd41ec439 Bump FRESCA version. 2015-12-17 14:28:24 -05:00
fc9cbab974 Choose template engine from active theme. 2015-12-17 11:04:29 -05:00
36f8010ebc Merge pull request #24 from fluentdesk/feat-notices
feat-notices
2015-12-17 10:27:58 -05:00
e80d8fb5c8 Bump version. 2015-12-17 10:29:22 -05:00
eabab26eef Update file headers. 2015-12-17 10:15:59 -05:00
74 changed files with 4149 additions and 1496 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/
tests/sandbox/
docs/
npm-debug.log

9
.travis.yml Normal file
View File

@ -0,0 +1,9 @@
language: node_js
node_js:
- "0.10"
- "0.11"
- "0.12"
- "4.0"
- "4.1"
- "4.2"
- "5.0"

58
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,58 @@
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
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
and ensures that your changes will be accepted once submitted.
2. **Fork and clone** the HackMyResume project.
3. Ideally, **create a new feature branch** (eg, `feat/new-awesome-feature` or
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`.
7. **Push** your commits.
7. **Submit a pull request** from your feature branch to the HackMyResume `dev`
branch.
8. We'll typically **respond** within 24 hours.
9. Your awesome changes will be **merged** after verification.
## Project Maintainers
HackMyResume is currently maintained by [hacksalot][ha] with assistance from
[tomheon][th] and our awesome [contributors][awesome]. Please direct all official
or internal inquiries to:
```
admin@hackmyresume.com
```
You can reach hacksalot directly at:
```
hacksalot@indevious.com
```
Thanks! See you out there in the trenches.
[fcv]: https://github.com/fluentdesk/fluentcv
[flow]: https://guides.github.com/introduction/flow/
[iss]: https://github.com/hacksalot/HackMyResume/issues
[ha]: https://github.com/hacksalot
[th]: https://github.com/tomheon
[awesome]: https://github.com/hacksalot/HackMyResume/graphs/contributors

97
FAQ.md Normal file
View File

@ -0,0 +1,97 @@
Frequently Asked Questions (FAQ)
================================
## How do I get started with HackMyResume?
1. Install with NPM: `[sudo] npm install hackmyresume -g`.
2. Create a new resume with: `hackmyresume NEW <resume-name>.json`.
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.
## 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.
## 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.
## 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.
## Should I use the FRESH or JSON Resume format/schema for my resume?
Both! The workflow we like to use:
1. Create a resume in FRESH format for tooling and analysis.
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.
## How do I use a FRESH theme?
FRESH themes currently come preinstalled with HackMyResume.
1. Specify the theme name in the `--theme` or `-t` parameter to the **build** command:
```bash
hackmyresume BUILD my-resume.json --theme <theme-name>`
```
`<theme-name>` can be one of `positive`, `compact`, `modern`, `minimist`, `hello-world`, or `awesome`.
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.
## How do I use a JSON Resume theme?
1. Install the theme locally. The easiest way to do that is with NPM.
```bash
npm install jsonresume-theme-classy
```
2. Pass the theme folder path into HackMyResume:
```bash
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.
## 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.
## 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:
```javascript
// myoptions.json
{
"sectionTitles": {
"employment": "empleo",
"skills": "habilidades",
"education": "educación"
}
}
```
Then, pass the options file into the `-o` or `--opts` parameter:
```bash
hackmyresume BUILD resume.json -o myoptions.json
```
This ability is currently only supported for FRESH resume themes.
## The HackMyResume terminal color scheme is giving me a headache! Can I disable it?
Yes. Use the `--no-color` option to disable terminal colors:
`hackmyresume <somecommand> <someoptions> --no-color`

View File

@ -14,9 +14,11 @@ module.exports = function (grunt) {
ui: 'bdd',
reporter: 'spec'
},
all: { src: ['tests/*.js'] }
all: { src: ['test/*.js'] }
},
clean: ['test/sandbox'],
yuidoc: {
compile: {
name: '<%= pkg.name %>',
@ -36,7 +38,7 @@ module.exports = function (grunt) {
laxcomma: true,
expr: true
},
all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js']
all: ['Gruntfile.js', 'src/**/*.js', 'test/*.js']
}
};
@ -46,11 +48,12 @@ module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-simple-mocha');
grunt.loadNpmTasks('grunt-contrib-yuidoc');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.registerTask('test', 'Test the FluentCV library.',
function( config ) { grunt.task.run( ['simplemocha:all'] ); });
grunt.registerTask('document', 'Generate FluentCV library documentation.',
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', [ 'jshint', 'test', 'yuidoc' ]);
grunt.registerTask('default', [ 'test', 'yuidoc' ]);
};

371
README.md
View File

@ -1,12 +1,19 @@
fluentCV
========
*Create polished technical résumés and CVs in multiple formats from your command
line or shell. See [FluentCV Desktop][7] for the desktop version. OS X ~ Windows
~ Linux.*
HackMyResume
============
![](assets/fluentcv_cli_ubuntu.png)
[![Latest release][img-release]][latest-release]
[![Build status (MASTER)][img-master]][travis-url-master]
[![Build status (DEV)][img-dev]][travis-url-dev]
FluentCV is a dev-friendly Swiss Army knife for resumes and CVs. Use it to:
*Create polished résumés and CVs in multiple formats from your command line or
shell. Author in clean Markdown and JSON, export to Word, HTML, PDF, LaTeX,
plain text, and other arbitrary formats. Fight the power, save trees. Compatible
with [FRESH][fresca] and [JRS][6] resumes.*
![](assets/resume-bouqet.png)
HackMyResume is a dev-friendly, local-only Swiss Army knife for resumes and CVs.
Use it to:
1. **Generate** HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML,
YAML, print, smoke signal, carrier pigeon, and other arbitrary-format resumes
@ -14,86 +21,127 @@ and CVs, from a single source of truth&mdash;without violating DRY.
2. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats.
3. **Validate** resumes against either format.
FluentCV supports both the [FRESH][fresca] and [JSON Resume][6] source formats.
HackMyResume is built with Node.js and runs on recent versions of OS X, Linux,
or Windows. View the [FAQ](FAQ.md).
## Features
- OS X, Linux, and Windows.
- Choose from dozens of FRESH or JSON Resume themes.
- Private, local-only resume authoring and analysis.
- Analyze your resume for keywords, gaps, and other metrics.
- Store your resume data as a durable, versionable JSON or YAML document.
- Generate polished resumes in multiple formats without violating [DRY][dry].
- Output to HTML, Markdown, LaTeX, PDF, MS Word, JSON, YAML, plain text, or XML.
- Compatible with [FRESH][fresh], [JSON Resume][6], [FRESCA][fresca], and
[FCV Desktop][7].
- Validate resumes against the FRESH or JSON Resume schema.
- Support for multiple input and output resumes.
- Convert between FRESH and JSON Resume resumes.
- Use from your command line or [desktop][7].
- Free and open-source through the MIT license.
- Updated daily / weekly. Contributions are [welcome](CONTRIBUTING.md).
## Install
Install FluentCV with NPM:
Install the latest stable version of HackMyResume with NPM:
```bash
[sudo] npm install fluentcv -g
[sudo] npm install hackmyresume -g
```
Note: for PDF generation you'll need to install a copy of [wkhtmltopdf][3] for
your platform. For LaTeX generation you'll need a valid LaTeX environment with
access to `xelatex` and similar.
Power users can install the latest bleeding-edge version (updated daily):
```bash
[sudo] npm install hacksalot/hackmyresume#dev -g
```
**For PDF generation**, you'll need to install a copy of [wkhtmltopdf][3] and/or PhantomJS for your platform.
## Installing Themes
HackMyResume supports both [FRESH][fresh-themes] and [JSON Resume][jrst]-style
résumé themes.
- FRESH themes currently come preinstalled with HackMyResume.
- JSON Resume themes can be installed from NPM, GitHub, or manually.
To install a JSON Resume theme, just `cd` to the folder where you want to store
your themes and run one of:
```bash
# Install with NPM
npm install jsonresume-theme-[theme-name]
# Install with GitHub
git clone https://github.com/[user-or-org]/[repo-name]
```
Then when you're ready to generate your resume, just reference the location of
the theme folder as you installed it:
```bash
hackmyresume BUILD resume.json TO out/resume.all -t node_modules/jsonresume-theme-classy
```
Note: You can use install themes anywhere on your file system. You don't need a
package.json or other NPM/Node infrastructure.
## Getting Started
To use FluentCV you'll need to create a valid resume in either [FRESH][fresca]
or [JSON Resume][6] format. Then you can start using the command line tool.
There are four basic commands you should be aware of:
To use HackMyResume you'll need to create a valid resume in either
[FRESH][fresca] or [JSON Resume][6] format. Then you can start using the command
line tool. There are five basic commands you should be aware of:
- `build` generates resumes in HTML, Word, Markdown, PDF, and other formats. Use
it when you need to submit, upload, print, or email resumes in specific formats.
- **build** generates resumes in HTML, Word, Markdown, PDF, and other formats.
Use it when you need to submit, upload, print, or email resumes in specific
formats.
```bash
# fluentcv BUILD <INPUTS> TO <OUTPUTS> [-t THEME]
fluentcv BUILD resume.json TO out/resume.all
fluentcv BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.all
# 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
```
- `new` creates a new resume in FRESH or JSON Resume format.
- **new** creates a new resume in FRESH or JSON Resume format.
```bash
# fluentcv NEW <OUTPUTS> [-f <FORMAT>]
fluentcv NEW resume.json
fluentcv NEW resume.json -f fresh
fluentcv NEW r1.json r2.json -f jrs
# hackmyresume NEW <OUTPUTS> [-f <FORMAT>]
hackmyresume NEW resume.json
hackmyresume NEW resume.json -f fresh
hackmyresume NEW r1.json r2.json -f jrs
```
- `convert` converts your source resume between FRESH and JSON Resume formats.
Use it to convert between the two formats to take advantage of tools and services.
- **analyze** inspects your resume for keywords, duration, and other metrics.
- **convert** converts your source resume between FRESH and JSON Resume
formats. Use it to convert between the two formats to take advantage of tools
and services.
```bash
# fluentcv CONVERT <INPUTS> TO <OUTPUTS>
fluentcv CONVERT resume.json TO resume-jrs.json
fluentcv CONVERT 1.json 2.json 3.json TO out/1.json out/2.json out/3.json
# 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
```
- `validate` validates the specified resume against either the FRESH or JSON
- **validate** validates the specified resume against either the FRESH or JSON
Resume schema. Use it to make sure your resume data is sufficient and complete.
```bash
# fluentcv VALIDATE <INPUTS>
fluentcv VALIDATE resume.json
fluentcv VALIDATE r1.json r2.json r3.json
# hackmyresume VALIDATE <INPUTS>
hackmyresume VALIDATE resume.json
hackmyresume VALIDATE r1.json r2.json r3.json
```
## Supported Output Formats
FluentCV supports these output formats:
HackMyResume supports these output formats:
Output Format | Ext | Notes
------------- | --- | -----
HTML | .html | A standard HTML 5 + CSS resume format that can be viewed in a browser, deployed to a website, etc.
Markdown | .md | A structured Markdown document that can be used as-is or used to generate HTML.
LaTeX | .tex | A structured LaTeX document (or collection of documents).
MS Word | .doc | A Microsoft Word office document.
Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme.
LaTeX | .tex | A structured LaTeX document (or collection of documents) that can be processed with pdflatex, xelatex, and similar tools.
MS Word | .doc | A Microsoft Word office document (XML-driven; WordProcessingML).
Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme (through wkhtmltopdf).
plain text | .txt | A formatted plain text document appropriate for emails or copy-paste.
JSON | .json | A JSON representation of the resume.
YAML | .yml | A YAML representation of the resume.
@ -101,22 +149,13 @@ RTF | .rtf | Forthcoming.
Textile | .textile | Forthcoming.
image | .png, .bmp | Forthcoming.
## Install
FluentCV requires a recent version of [Node.js][4] and [NPM][5]. Then:
1. Install the latest official [wkhtmltopdf][3] binary for your platform.
2. Optionally install an updated LaTeX environment (LaTeX resumes only).
2. Install **fluentCV** with `[sudo] npm install fluentcv -g`.
3. You're ready to go.
## Use
Assuming you've got a JSON-formatted resume handy, generating resumes in
different formats and combinations easy. Just run:
```bash
fluentcv BUILD <INPUTS> <OUTPUTS> [-t theme].
hackmyresume BUILD <INPUTS> <OUTPUTS> [-t theme].
```
Where `<INPUTS>` is one or more .json resume files, separated by spaces;
@ -125,25 +164,25 @@ theme (default to Modern). For example:
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.)
fluentcv build resume.json -o out/resume.all -t modern
hackmyresume BUILD resume.json TO out/resume.all -t modern
# Generate a specific resume format
fluentcv build resume.json TO out/resume.html
fluentcv build resume.json TO out/resume.pdf
fluentcv build resume.json TO out/resume.md
fluentcv build resume.json TO out/resume.doc
fluentcv build resume.json TO out/resume.json
fluentcv build resume.json TO out/resume.txt
fluentcv build resume.json TO out/resume.yml
hackmyresume BUILD resume.json TO out/resume.html
hackmyresume BUILD resume.json TO out/resume.pdf
hackmyresume BUILD resume.json TO out/resume.md
hackmyresume BUILD resume.json TO out/resume.doc
hackmyresume BUILD resume.json TO out/resume.json
hackmyresume BUILD resume.json TO out/resume.txt
hackmyresume BUILD resume.json TO out/resume.yml
# Specify 2 inputs and 3 outputs
fluentcv build in1.json in2.json TO out.html out.doc out.pdf
hackmyresume BUILD in1.json in2.json TO out.html out.doc out.pdf
```
You should see something to the effect of:
```
*** FluentCV v0.9.0 ***
*** HackMyResume v1.4.0 ***
Reading JSON resume: foo/resume.json
Applying MODERN Theme (7 formats)
Generating HTML resume: out/resume.html
@ -159,28 +198,44 @@ Generating YAML resume: out/resume.yml
### Applying a theme
You can specify a predefined or custom theme via the optional `-t` parameter. For a predefined theme, include the theme name. For a custom theme, include the path to the custom theme's folder.
HackMyResume can work with any FRESH or JSON Resume theme. To specify a theme
when generating your resume, use the `-t` or `--theme` parameter:
```bash
fluentcv build resume.json -t modern
fluentcv build resume.json -t ~/foo/bar/my-custom-theme/
hackmyresume BUILD resume.json TO out/rez.all -t [theme]
```
As of v0.9.0, available predefined themes are `modern`, `minimist`, and `hello-world`, and `compact`.
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
```
As of v1.4.0, available predefined themes are `positive`, `modern`, `compact`,
`minimist`, and `hello-world`.
### Merging resumes
You can **merge multiple resumes together** by specifying them in order from most generic to most specific:
You can **merge multiple resumes together** by specifying them in order from
most generic to most specific:
```bash
# Merge specific.json onto base.json and generate all formats
fluentcv build base.json specific.json -o resume.all
hackmyresume BUILD base.json specific.json TO resume.all
```
This can be useful for overriding a base (generic) resume with information from a specific (targeted) resume. For example, you might override your generic catch-all "software developer" resume with specific details from your targeted "game developer" resume, or combine two partial resumes into a "complete" resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge:
This can be useful for overriding a base (generic) resume with information from
a specific (targeted) resume. For example, you might override your generic
catch-all "software developer" resume with specific details from your targeted
"game developer" resume, or combine two partial resumes into a "complete"
resume. Merging follows conventional [extend()][9]-style behavior and there's
no arbitrary limit to how many resumes you can merge:
```bash
fluentcv build in1.json in2.json in3.json in4.json TO out.html out.doc
hackmyresume BUILD in1.json in2.json in3.json in4.json TO out.html out.doc
Reading JSON resume: in1.json
Reading JSON resume: in2.json
Reading JSON resume: in3.json
@ -192,72 +247,179 @@ Generating WORD resume: out.doc
### Multiple targets
You can specify **multiple output targets** and FluentCV will build them:
You can specify **multiple output targets** and HackMyResume will build them:
```bash
# Generate out1.doc, out1.pdf, and foo.txt from me.json.
fluentcv build me.json -o out1.doc -o out1.pdf -o foo.txt
```
You can also omit the output file(s) and/or theme completely:
```bash
# Equivalent to "fluentcv resume.json resume.all -t modern"
fluentcv build resume.json
hackmyresume BUILD me.json TO out1.doc out1.pdf foo.txt
```
### Using .all
The special `.all` extension tells FluentCV to generate all supported output formats for the given resume. For example, this...
The special `.all` extension tells HackMyResume to generate all supported output
formats for the given resume. For example, this...
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, etc.)
fluentcv build me.json -o out/resume.all
hackmyresume BUILD me.json TO out/resume.all
```
..tells FluentCV to read `me.json` and generate `out/resume.md`, `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`.
..tells HackMyResume to read `me.json` and generate `out/resume.md`,
`out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and
`out/resume.json`.
### Analyzing
HackMyResume can analyze your resume for keywords, employment gaps, and other
metrics. Run:
```bash
hackmyresume ANALYZE <my-resume>.json
```
Depending on the HackMyResume version, you should see output similar to:
```
*** HackMyResume v1.4.1 ***
Reading resume: resume.json
Analyzing FRESH resume: resume.json
SECTIONS (10):
employment: 12
education: 2
service: 1
skills: 8
writing: 1
recognition: 0
social: 4
interests: 2
references: 1
languages: 2
COVERAGE (61.1%):
Total Days: 6034
Employed: 3688
Gaps: 8 [31, 1065, 273, 153, 671, 61, 61, 31]
Overlaps: 1 [243]
KEYWORDS (61):
Node.js: 6 mentions
JavaScript: 9 mentions
SQL Server: 3 mentions
Visual Studio: 6 mentions
Web API: 1 mentions
N-tier / 3-tier: 1 mentions
HTML 5: 1 mentions
JavaScript: 6 mentions
CSS: 2 mentions
Sass / LESS / SCSS: 1 mentions
LAMP: 3 mentions
WISC: 1 mentions
HTTP: 21 mentions
JSON: 1 mentions
XML: 2 mentions
REST: 1 mentions
WebSockets: 2 mentions
Backbone.js: 3 mentions
Angular.js: 1 mentions
Node.js: 4 mentions
NPM: 1 mentions
Bower: 1 mentions
Grunt: 2 mentions
Gulp: 1 mentions
jQuery: 2 mentions
Bootstrap: 3 mentions
Underscore.js: 1 mentions
PhantomJS: 1 mentions
CoffeeScript: 1 mentions
Python: 11 mentions
Perl: 4 mentions
PHP: 7 mentions
MySQL: 12 mentions
PostgreSQL: 4 mentions
NoSQL: 2 mentions
Apache: 2 mentions
AWS: 2 mentions
EC2: 2 mentions
RDS: 3 mentions
S3: 1 mentions
Azure: 1 mentions
Rackspace: 1 mentions
C++: 23 mentions
C++ 11: 1 mentions
Boost: 1 mentions
Xcode: 2 mentions
gcc: 1 mentions
OO&AD: 1 mentions
.NET: 20 mentions
Unity 5: 2 mentions
Mono: 3 mentions
MonoDevelop: 1 mentions
Xamarin: 1 mentions
TOTAL: 180 mentions
```
### Validating
FluentCV can also validate your resumes against either the [FRESH /
HackMyResume can also validate your resumes against either the [FRESH /
FRESCA][fresca] or [JSON Resume][6] formats. To validate one or more existing
resumes, use the `validate` command:
```bash
# Validate myresume.json against either the FRESH or JSON Resume schema.
fluentcv validate resumeA.json resumeB.json
hackmyresume VALIDATE resumeA.json resumeB.json
```
FluentCV will validate each specified resume in turn:
HackMyResume will validate each specified resume in turn:
```bash
*** FluentCV v0.9.0 ***
*** HackMyResume v0.9.0 ***
Validating JSON resume: resumeA.json (INVALID)
Validating JSON resume: resumeB.json (VALID)
```
### Converting
FluentCV can convert between the [FRESH][fresca] and [JSON Resume][6] formats.
Just run:
HackMyResume can convert between the [FRESH][fresca] and [JSON Resume][6]
formats. Just run:
```bash
fluentcv CONVERT <INPUTS> <OUTPUTS>
hackmyresume CONVERT <INPUTS> <OUTPUTS>
```
where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and
<OUTPUTS> is a corresponding list of output file names. FluentCV will autodetect
the format (FRESH or JRS) of each input resume and convert it to the other
format (JRS or FRESH).
<OUTPUTS> is a corresponding list of output file names. HackMyResume will
autodetect the format (FRESH or JRS) of each input resume and convert it to the
other format (JRS or FRESH).
### External options
Starting in v1.4.x you can pass options into HackMyResume via an external
options or ".hackmyrc" file.
```javascript
{
// Set the default theme to "compact"
"theme": "compact",
// Change the "employment" section title text to "Work"
"sectionTitles": {
"employment": "Work"
}
}
```
### Prettifying
FluentCV applies [js-beautify][10]-style HTML prettification by default to
HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag
can be used:
HackMyResume applies [js-beautify][10]-style HTML prettification by default to
HTML-formatted resumes. To disable prettification, the `--no-prettify` or `-n`
flag can be used:
```bash
fluentcv generate resume.json out.all --nopretty
hackmyresume BUILD resume.json out.all --no-prettify
```
### Silent Mode
@ -265,10 +427,16 @@ fluentcv generate resume.json out.all --nopretty
Use `-s` or `--silent` to run in silent mode:
```bash
fluentcv generate resume.json -o someFile.all -s
fluentcv generate resume.json -o someFile.all --silent
hackmyresume BUILD resume.json -o someFile.all -s
hackmyresume BUILD resume.json -o someFile.all --silent
```
## Contributing
HackMyResume is a community-driven free and open source project under the MIT
License. Contributions are encouraged and we respond to all PRs and issues,
usually within 24 hours. See [CONTRIBUTING.md][contribute] for details.
## License
MIT. Go crazy. See [LICENSE.md][1] for details.
@ -286,3 +454,12 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
[fresh]: https://github.com/fluentdesk/FRESH
[fresca]: https://github.com/fluentdesk/FRESCA
[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
[img-release]: https://img.shields.io/github/release/hacksalot/HackMyResume.svg?label=version
[img-master]: https://img.shields.io/travis/hacksalot/HackMyResume/master.svg
[img-dev]: https://img.shields.io/travis/hacksalot/HackMyResume/dev.svg?label=dev
[travis-url-master]: https://travis-ci.org/hacksalot/HackMyResume?branch=master
[travis-url-dev]: https://travis-ci.org/hacksalot/HackMyResume?branch=dev
[latest-release]: https://github.com/hacksalot/HackMyResume/releases/latest
[contribute]: CONTRIBUTING.md
[fresh-themes]: https://github.com/fluentdesk/fresh-themes
[jrst]: https://www.npmjs.com/search?q=jsonresume-theme

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

BIN
assets/hackmyresume_cli.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/resume-bouqet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

@ -1,10 +1,14 @@
{
"name": "fluentcv",
"version": "0.10.2",
"name": "hackmyresume",
"version": "1.4.2",
"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/fluentdesk/fluentcv.git"
"url": "https://github.com/hacksalot/HackMyResume.git"
},
"scripts": {
"test": "grunt clean && mocha",
"grunt": "grunt"
},
"keywords": [
"resume",
@ -18,46 +22,75 @@
"PDF",
"YAML",
"HTML",
"CLI"
"LaTeX",
"CLI",
"Handlebars",
"Underscore",
"template"
],
"author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
"contributors": [
"jjanusch (https://github.com/driftdev)",
"robertmain (https://github.com/robertmain)",
"tomheon (https://github.com/tomheon)",
"zhuangya (https://github.com/zhuangya)",
"hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)"
],
"author": "James M. Devlin",
"license": "MIT",
"preferGlobal": "true",
"bugs": {
"url": "https://github.com/fluentdesk/fluentcv/issues"
"url": "https://github.com/hacksalot/HackMyResume/issues"
},
"main": "src/fluentcmd.js",
"main": "src/hackmyapi.js",
"bin": {
"fluentcv": "src/index.js"
"hackmyresume": "src/index.js"
},
"homepage": "https://github.com/fluentdesk/fluentcv",
"homepage": "https://github.com/hacksalot/HackMyResume",
"dependencies": {
"colors": "^1.1.2",
"fluent-themes": "~0.6.2-beta",
"fresca": "~0.2.1",
"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",
"handlebars": "^4.0.5",
"html": "0.0.10",
"is-my-json-valid": "^2.12.2",
"json-lint": "^0.1.0",
"jst": "0.0.13",
"lodash": "^3.10.1",
"marked": "^0.3.5",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"moment": "^2.10.6",
"parse-filepath": "^0.6.3",
"path-exists": "^2.1.0",
"phantom": "^0.8.4",
"recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.2.0",
"slash": "^1.0.0",
"string-padding": "^1.0.2",
"string.prototype.startswith": "^0.2.0",
"underscore": "^1.8.3",
"webshot": "^0.16.0",
"wkhtmltopdf": "^0.1.5",
"word-wrap": "^1.1.0",
"xml-escape": "^1.0.0",
"yamljs": "^0.2.4"
},
"devDependencies": {
"chai": "*",
"fresh-test-resumes": "^0.2.1",
"grunt": "*",
"grunt-cli": "^0.1.13",
"grunt-contrib-clean": "^0.7.0",
"grunt-contrib-jshint": "^0.11.3",
"grunt-contrib-yuidoc": "^0.10.0",
"grunt-simple-mocha": "*",
"is-my-json-valid": "^2.12.2",
"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"
}

View File

@ -1,9 +1,18 @@
/**
FRESH to JSON Resume conversion routiens.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@license MIT. See LICENSE.md for details.
@module convert.js
*/
(function(){
(function(){ // TODO: refactor everything
var _ = require('underscore');
/**
Convert between FRESH and JRS resume/CV formats.
@ -12,34 +21,32 @@ FRESH to JSON Resume conversion routiens.
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,
@ -47,7 +54,6 @@ FRESH to JSON Resume conversion routiens.
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),
@ -62,17 +68,19 @@ FRESH to JSON Resume conversion routiens.
};
},
/**
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,
@ -89,9 +97,9 @@ FRESH to JSON Resume conversion routiens.
countryCode: src.location.country,
region: src.location.region
},
profiles: social( src.social, false )
profiles: social( src.social, false ),
imp: src.imp
},
work: employment( src.employment, false ),
education: education( src.education, false ),
skills: skillsToJRS( src.skills, false ),
@ -103,14 +111,36 @@ FRESH to JSON Resume conversion routiens.
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";
@ -119,7 +149,10 @@ FRESH to JSON Resume conversion routiens.
return obj;
}
function employment( obj, direction ) {
if( !obj ) return obj;
if( !direction ) {
return obj && obj.history ?
obj.history.map(function(emp){
@ -142,11 +175,12 @@ FRESH to JSON Resume conversion routiens.
position: job.position,
employer: job.company,
summary: job.summary,
current: (!job.endDate || !job.endDate.trim() || job.endDate.trim().toLowerCase() === 'current') || undefined,
current: (!job.endDate || !job.endDate.trim() ||
job.endDate.trim().toLowerCase() === 'current') || undefined,
start: job.startDate,
end: job.endDate,
url: job.website,
keywords: "",
keywords: [],
highlights: job.highlights
};
}) : undefined
@ -155,9 +189,12 @@ FRESH to JSON Resume conversion routiens.
}
function education( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj && obj.length ? {
level: "",
history: obj.map(function(edu){
return {
institution: edu.institution,
@ -165,8 +202,8 @@ FRESH to JSON Resume conversion routiens.
end: edu.endDate,
grade: edu.gpa,
curriculum: edu.courses,
url: edu.website || edu.url || null,
summary: null,
url: edu.website || edu.url || undefined,
summary: edu.summary || "",
area: edu.area,
studyType: edu.studyType
};
@ -189,7 +226,10 @@ FRESH to JSON Resume conversion routiens.
}
}
function service( obj, direction, foreign ) {
if( !obj ) return obj;
if( direction ) {
return {
history: obj && obj.length ? obj.map(function(vol) {
@ -223,7 +263,10 @@ FRESH to JSON Resume conversion routiens.
}
}
function social( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj.map(function(pro){
return {
@ -245,7 +288,10 @@ FRESH to JSON Resume conversion routiens.
}
}
function recognition( obj, direction, foreign ) {
if( !obj ) return obj;
if( direction ) {
return obj && obj.length ? obj.map(
function(awd){
@ -273,7 +319,10 @@ FRESH to JSON Resume conversion routiens.
}
}
function references( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj && obj.length && obj.map(function(ref){
return {
@ -294,7 +343,10 @@ FRESH to JSON Resume conversion routiens.
}
}
function writing( obj, direction ) {
if( !obj ) return obj;
if( direction ) {
return obj.map(function( pub ) {
return {
@ -311,7 +363,8 @@ FRESH to JSON Resume conversion routiens.
return obj && obj.length ? obj.map(function(pub){
return {
name: pub.title,
publisher: pub.publisher && pub.publisher.name ? pub.publisher.name : pub.publisher,
publisher: pub.publisher &&
pub.publisher.name ? pub.publisher.name : pub.publisher,
releaseDate: pub.date,
website: pub.url,
summary: pub.summary
@ -320,10 +373,12 @@ FRESH to JSON Resume conversion routiens.
}
}
function skillsToFRESH( skills ) {
function skillsToFRESH( skills ) {
if( !skills ) return skills;
return {
sets: skills.map(function(set) {
sets: skills.map(function( set ) {
return {
name: set.name,
level: set.level,
@ -333,7 +388,10 @@ FRESH to JSON Resume conversion routiens.
};
}
function skillsToJRS( skills ) {
if( !skills ) return skills;
var ret = [];
if( skills.sets && skills.sets.length ) {
ret = skills.sets.map(function(set){

View File

@ -0,0 +1,21 @@
(function(){
/**
Supported resume formats.
*/
module.exports = [
{ name: 'html', ext: 'html', gen: new (require('../gen/html-generator'))() },
{ name: 'txt', ext: 'txt', gen: new (require('../gen/text-generator'))() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../gen/word-generator'))() },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new (require('../gen/html-pdf-generator'))() },
{ name: 'png', ext: 'png', fmt: 'html', is: false, gen: new (require('../gen/html-png-generator'))() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new (require('../gen/markdown-generator'))() },
{ name: 'json', ext: 'json', gen: new (require('../gen/json-generator'))() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../gen/json-yaml-generator'))() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../gen/latex-generator'))() }
];
}());

View File

@ -0,0 +1,13 @@
(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,184 +0,0 @@
{
"name": "",
"meta": {
"format": "FRESH@0.1.0",
"version": "0.1.0"
},
"info": {
"label": "",
"characterClass": "",
"brief": "",
"image": ""
},
"contact": {
"website": "",
"phone": "",
"email": "",
"other": []
},
"location": {
"address": "",
"city": "",
"region": "",
"code": "",
"country": ""
},
"social": [
{
"label": "",
"network": "",
"user": "",
"url": ""
}
],
"employment": {
"summary": "",
"history": [
{
"employer": "",
"url": "",
"position": "",
"summary": "",
"start": "",
"end": "",
"keywords": [],
"highlights": []
}
]
},
"education": {
"summary": "",
"level": "",
"degree": "",
"history": [
{
"institution": "",
"url": "",
"start": "",
"end": "",
"grade": "",
"summary": "",
"curriculum": []
}
]
},
"service": {
"summary": "",
"history": [
{
"flavor": "",
"position": "",
"organization": "",
"url": "",
"start": "",
"end": "",
"summary": "",
"highlights": []
}
]
},
"skills": {
"sets": [
{
"name": "",
"level": "",
"skills": []
}
],
"list": [ ]
},
"samples": [
{
"title": "",
"summary": "",
"url": "",
"date": ""
}
],
"writing": [
{
"title": "",
"flavor": "",
"date": "",
"publisher": {
"name": "",
"url": ""
},
"url": ""
}
],
"reading": [
{
"title": "",
"flavor": "",
"url": "",
"author": ""
}
],
"recognition": [
{
"flavor": "",
"from": "",
"title": "",
"event": "",
"date": "",
"summary": ""
}
],
"references": [
{
"name": "",
"flavor": "",
"private": true,
"contact": [
{
"label": "",
"flavor": "",
"value": ""
}
]
}
],
"testimonials": [
{
"name": "",
"flavor": "",
"quote": ""
}
],
"languages": [
{
"language": "",
"level": "",
"years": 0
}
],
"interests": [
{
"name": "",
"summary": "",
"keywords": []
}
]
}

128
src/core/error-handler.js Normal file
View File

@ -0,0 +1,128 @@
/**
Error-handling routines for HackMyResume.
@module error-handler.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var HACKMYSTATUS = require('./status-codes')
, PKG = require('../../package.json')
, FS = require('fs')
, FCMD = require('../hackmyapi')
, PATH = require('path')
, chalk = require('chalk');
/**
An amorphous blob of error handling code for HackMyResume.
@class ErrorHandler
*/
var ErrorHandler = module.exports = {
err: function( ex, shouldExit ) {
var msg = '', exitCode;
if( ex.handled ) {
if( shouldExit )
process.exit( exitCode );
return;
}
if( ex.fluenterror ){
switch( ex.fluenterror ) {
case HACKMYSTATUS.themeNotFound:
msg = "The specified theme couldn't be found: " + ex.data;
break;
case HACKMYSTATUS.copyCSS:
msg = "Couldn't copy CSS file to destination folder";
break;
case HACKMYSTATUS.resumeNotFound:
msg = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
chalk.yellow(' in FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.missingCommand:
msg = chalk.yellow("Please ") + chalk.yellow.bold("give me a command") +
chalk.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, '../use.txt'), 'utf8' ));
break;
case HACKMYSTATUS.invalidCommand:
msg = chalk.yellow('Invalid command: "') + chalk.yellow.bold(ex.attempted) + chalk.yellow('"');
break;
case HACKMYSTATUS.resumeNotFoundAlt:
msg = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
chalk.yellow(' in either FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.inputOutputParity:
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify an output file name') +
chalk.yellow(' for every input file you wish to convert.');
break;
case HACKMYSTATUS.createNameMissing:
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify the filename of the resume') +
chalk.yellow(' to create.');
break;
case HACKMYSTATUS.wkhtmltopdf:
msg = chalk.red.bold('ERROR: PDF generation failed. ') + chalk.red('Make sure wkhtmltopdf is ' +
'installed and accessible from your path.');
break;
case HACKMYSTATUS.invalid:
msg = chalk.red.bold('ERROR: Validation failed and the --assert option was specified.');
break;
}
exitCode = ex.fluenterror;
}
else {
msg = ex.toString();
exitCode = 4;
}
// Deal with pesky 'Error:' prefix.
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
// If this is an unhandled error, or a specific class of handled error,
// output the error message and stack.
if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s
console.log( chalk.red.bold('ERROR: ' + trimmed.toString()) );
if( ex.code !== 'ENOENT' ) // Don't emit stack for common stuff
console.log( chalk.gray(ex.stack) );
}
else {
console.log( trimmed.toString() );
}
// Let the error code be the process's return code.
if( shouldExit || ex.shouldExit )
process.exit( exitCode );
}
};
}());

View File

@ -1,6 +1,7 @@
/**
The FluentCV date representation.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
The HackMyResume date representation.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module fluent-date.js
*/
var moment = require('moment');
@ -12,7 +13,7 @@ formats to be aware of here.
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 FluentCV "mmm YYYY" format ("Mar 2015" or "Dec 2008")
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

View File

@ -1,32 +1,42 @@
/**
Definition of the FRESHResume class.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@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-style resume in JSON or YAML.
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() {
}
/**
Open and parse the specified FRESH resume sheet. Merge the JSON object model
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
Initialize the FreshResume from file.
*/
FreshResume.prototype.open = function( file, title ) {
this.imp = { fileName: file };
@ -34,122 +44,24 @@ Definition of the FRESHResume class.
return this.parse( this.imp.raw, title );
};
/**
Save the sheet to disk (for environments that have disk access).
*/
FreshResume.prototype.save = function( filename ) {
this.imp.fileName = filename || this.imp.fileName;
FS.writeFileSync( this.imp.fileName, this.stringify(), 'utf8' );
return this;
};
/**
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
FreshResume.prototype.saveAs = function( filename, format ) {
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;
};
FreshResume.prototype.dupe = function() {
var rnew = new FreshResume();
rnew.parse( this.stringify(), { } );
return rnew;
};
/**
Convert the supplied object to a JSON string, sanitizing meta-properties along
the way.
*/
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 );
};
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
FreshResume.prototype.markdownify = function() {
var that = this;
var ret = this.dupe();
function MDIN(txt){
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
// TODO: refactor recursion
function markdownifyStringsInObject( 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 ? MDIN(elem) : MD( elem );
else
markdownifyStringsInObject( elem );
});
}
else if (typeof obj === 'object') {
Object.keys( obj ).forEach(function(key) {
var sub = obj[key];
if( typeof sub === 'string' || sub instanceof String ) {
if( _.contains(['skills','url','start','end','date'], key) )
return;
if( key === 'summary' )
obj[key] = MD( obj[key] );
else
obj[key] = inline ? MDIN( obj[key] ) : MD( obj[key] );
}
else
markdownifyStringsInObject( sub );
});
}
}
Object.keys( ret ).forEach(function(member){
markdownifyStringsInObject( ret[ member ] );
});
return ret;
};
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
FreshResume.prototype.stringify = function() {
return FreshResume.stringify( this );
};
/**
Open and parse the specified JSON resume sheet. Merge the JSON object model
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
Initialize the the FreshResume from string.
*/
FreshResume.prototype.parse = function( stringData, opts ) {
return this.parseJSON( JSON.parse( stringData ), opts );
};
// Parse the incoming JSON representation
var rep = JSON.parse( stringData );
/**
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 );
@ -176,27 +88,200 @@ Definition of the FRESHResume class.
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 = [];
this.skills && this.skills.length &&
(flatSkills = this.skills.map(function(sk) { return sk.name; }));
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;
},
/**
Update the sheet's raw data. TODO: remove/refactor
*/
FreshResume.prototype.updateData = function( str ) {
this.clear( false );
this.parse( str );
return this;
};
/**
Reset the sheet to an empty state.
Reset the sheet to an empty state. TODO: refactor/review
*/
FreshResume.prototype.clear = function( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
@ -213,6 +298,8 @@ Definition of the FRESHResume class.
delete this.social;
};
/**
Get a safe count of the number of things in a section.
*/
@ -223,14 +310,17 @@ Definition of the FRESHResume class.
return obj.length || 0;
};
/**
Get the default (empty) sheet.
Get the default (starter) sheet.
*/
FreshResume.default = function() {
return new FreshResume().open(
PATH.join( __dirname, 'empty-fresh.json'), 'Empty' );
return new FreshResume().parseJSON( require('fresh-resume-starter') );
};
/**
Add work experience to the sheet.
*/
@ -251,6 +341,8 @@ Definition of the FRESHResume class.
return newObject;
};
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
@ -261,6 +353,8 @@ Definition of the FRESHResume class.
});
};
/**
Return the specified network profile.
*/
@ -271,6 +365,8 @@ Definition of the FRESHResume class.
});
};
/**
Return an array of profiles for the specified network, for when the user
has multiple eg. GitHub accounts.
@ -282,6 +378,8 @@ Definition of the FRESHResume class.
});
};
/**
Determine if the sheet includes a specific skill.
*/
@ -294,13 +392,15 @@ Definition of the FRESHResume class.
});
};
/**
Validate the sheet against the FRESH Resume schema.
*/
FreshResume.prototype.isValid = function( info ) {
var schemaObj = require('FRESCA');
var schemaObj = require('fresca');
var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // Note [1]
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 );
@ -311,6 +411,8 @@ Definition of the FRESHResume class.
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().
@ -319,30 +421,48 @@ Definition of the FRESHResume class.
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
FreshResume.prototype.duration = function() {
if( this.employment.history && this.employment.history.length ) {
var firstJob = _.last( this.employment.history );
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( this.employment.history, function( w ) {
var careerLast = _.max( empHist, function( w ) {
return( w.safe && w.safe.end ) ? w.safe.end.unix() : moment().unix();
});
return careerLast.safe.end.diff( careerStart, 'years' );
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( ) {
this.employment.history && this.employment.history.sort( byDateDesc );
this.education.history && this.education.history.sort( byDateDesc );
this.service.history && this.service.history.sort( byDateDesc );
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
@ -352,14 +472,10 @@ Definition of the FRESHResume class.
return( a.safe.date.isBefore(b.safe.date) ) ? 1
: ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0;
});
function byDateDesc(a,b) {
return( a.safe.start.isBefore(b.safe.start) ) ? 1
: ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0;
}
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
@ -405,11 +521,15 @@ Definition of the FRESHResume class.
}
/**
Export the Sheet function/ctor.
*/
module.exports = FreshResume;
}());
// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats

View File

@ -1,86 +1,115 @@
/**
Abstract theme representation.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
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')
, RECURSIVE_READ_DIR = require('recursive-readdir-sync');
, READFILES = require('recursive-readdir-sync');
/**
The Theme class is a representation of a FluentCV theme asset.
@class Theme
The FRESHTheme class is a representation of a FRESH theme
asset. See also: JRSTheme.
@class FRESHTheme
*/
function Theme() {
function FRESHTheme() {
}
/**
Open and parse the specified theme.
*/
Theme.prototype.open = function( themeFolder ) {
FRESHTheme.prototype.open = function( themeFolder ) {
this.folder = themeFolder;
// Open the [theme-name].json file; should have the same name as folder
this.folder = themeFolder;
var pathInfo = PATH.parse( themeFolder );
var themeFile = PATH.join( themeFolder, pathInfo.base + '.json' );
var 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 );
// Set up a formats has for the theme
var formatsHash = { };
// 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 = loadExplicit.call( this, formatsHash );
this.explicit = true;
}
else {
formatsHash = loadImplicit.call( this );
formatsHash = loadImplicit.call( this, formatsHash );
}
// Add freebie formats every theme gets
formatsHash.json = { title: 'json', outFormat: 'json', pre: 'json', ext: 'json', path: null, data: null };
formatsHash.yml = { title: 'yaml', outFormat: 'yml', pre: 'yml', ext: 'yml', path: null, data: null };
// Cache
this.formats = formatsHash;
// Set the official theme name
this.name = PATH.parse( this.folder ).name;
this.name = parsePath( this.folder ).name;
return this;
};
/**
Determine if the theme supports the specified output format.
*/
Theme.prototype.hasFormat = function( fmt ) {
FRESHTheme.prototype.hasFormat = function( fmt ) {
return _.has( this.formats, fmt );
};
/**
Determine if the theme supports the specified output format.
*/
Theme.prototype.getFormat = function( fmt ) {
FRESHTheme.prototype.getFormat = function( fmt ) {
return this.formats[ fmt ];
};
function loadImplicit() {
/**
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 formatsHash = { };
var that = this;
var major = false;
@ -90,16 +119,17 @@ Abstract theme representation.
// Iterate over all files in the theme folder, producing an array, fmts,
// containing info for each file. While we're doing that, also build up
// the formatsHash object.
var fmts = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) {
var 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 = PATH.parse(absPath);
var pathInfo = parsePath(absPath);
var outFmt = '', isMajor = false;
var portion = pathInfo.dir.replace(tplFolder,'');
var portion = pathInfo.dirname.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf|partials)(?:\/|\\)?/ig;
if( portion[1] === '_' ) return;
var reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
var res = reg.exec( portion );
if( res ) {
if( res[1] !== 'partials' ) {
@ -117,7 +147,7 @@ Abstract theme representation.
// compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-');
outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 );
outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1);
isMajor = true;
}
@ -132,8 +162,8 @@ Abstract theme representation.
action: 'transform',
path: absPath,
major: isMajor,
orgPath: PATH.relative(that.folder, absPath),
ext: pathInfo.ext.slice(1),
orgPath: PATH.relative(tplFolder, absPath),
ext: pathInfo.extname.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
@ -147,15 +177,30 @@ Abstract theme representation.
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){ return fmt && (fmt.ext === 'css'); }))
(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 ) {
// For each CSS file, get its corresponding HTML file
var idx = _.findIndex(fmts, function( fmt ) {
return fmt.pre === cssf.pre && fmt.ext === 'html';
return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
});
cssf.action = null;
fmts[ idx ].css = cssf.data;
fmts[ idx ].cssPath = cssf.path;
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
@ -166,33 +211,38 @@ Abstract theme representation.
return formatsHash;
}
function loadExplicit() {
var that = this;
// Set up a hash of formats supported by this theme.
var formatsHash = { };
// Establish the base theme folder
/**
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 = RECURSIVE_READ_DIR( tplFolder ).map( function( absPath ) {
var fmts = READFILES( tplFolder ).map( function( absPath ) {
act = null;
// If this file is mentioned in the theme's JSON file under "transforms"
var pathInfo = PATH.parse(absPath);
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;
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';
}
@ -201,7 +251,7 @@ Abstract theme representation.
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
if( !outFmt ) {
var portion = pathInfo.dir.replace(tplFolder,'');
var portion = pathInfo.dirname.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
var res = reg.exec( portion );
@ -213,7 +263,7 @@ Abstract theme representation.
// compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-');
outFmt = ( idx === -1 ) ? pathInfo.name : pathInfo.name.substr( idx + 1 );
outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1);
}
// We should have a valid output format now.
@ -229,7 +279,7 @@ Abstract theme representation.
action: act,
orgPath: PATH.relative(that.folder, absPath),
path: absPath,
ext: pathInfo.ext.slice(1),
ext: pathInfo.extname.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
@ -243,7 +293,11 @@ Abstract theme representation.
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){ return fmt.ext === 'css'; }))
(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 ) {
@ -261,12 +315,22 @@ Abstract theme representation.
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 = Theme;
module.exports = FRESHTheme;
}());

View File

@ -1,36 +1,42 @@
/**
Definition of the JRSResume class.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@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');
/**
The JRSResume class represent a specific JSON character sheet. When Sheet.open
is called, we merge the loaded JSON sheet properties onto the Sheet instance
via extend(), so a full-grown sheet object will have all of the methods here,
plus a complement of JSON properties from the backing JSON file. That allows
us to treat Sheet objects interchangeably with the loaded JSON model.
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() {
}
/**
Open and parse the specified JSON resume sheet. Merge the JSON object model
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
Initialize the JSResume from file.
*/
JRSResume.prototype.open = function( file, title ) {
//this.imp = { fileName: file }; <-- schema violation, tuck it into .basics instead
//this.imp = { fileName: file }; <-- schema violation, tuck it into .basics
this.basics = {
imp: {
fileName: file,
@ -40,48 +46,34 @@ Definition of the JRSResume class.
return this.parse( this.basics.imp.raw, title );
};
/**
Save the sheet to disk (for environments that have disk access).
*/
JRSResume.prototype.save = function( filename ) {
this.basics.imp.fileName = filename || this.basics.imp.fileName;
FS.writeFileSync( this.basics.imp.fileName, this.stringify( this ), 'utf8' );
return this;
};
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
JRSResume.stringify = function( obj ) {
function replacer( key,value ) { // Exclude these keys from stringification
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview', 'display_progress_bar'],
function( val ) { return key.trim() === val; }
) ? undefined : value;
}
return JSON.stringify( obj, replacer, 2 );
};
JRSResume.prototype.stringify = function() {
return JRSResume.stringify( this );
};
/**
Open and parse the specified JSON resume sheet. Merge the JSON object model
onto this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
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.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 );
@ -93,6 +85,71 @@ Definition of the JRSResume class.
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.
*/
@ -106,15 +163,20 @@ Definition of the JRSResume class.
return flatSkills;
};
/**
Update the sheet's raw data. TODO: remove/refactor
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.updateData = function( str ) {
this.clear( false );
this.parse( str );
return this;
JRSResume.prototype.i = function() {
this.basics = this.basics || { imp: { } };
return this.basics;
};
/**
Reset the sheet to an empty state.
*/
@ -132,13 +194,19 @@ Definition of the JRSResume class.
delete this.basics.profiles;
};
/**
Get the default (empty) sheet.
*/
JRSResume.default = function() {
return new JRSResume().open( PATH.join( __dirname, 'empty-jrs.json'), 'Empty' );
return new JRSResume().open(
PATH.join( __dirname, 'empty-jrs.json'), 'Empty'
);
};
/**
Add work experience to the sheet.
*/
@ -150,6 +218,8 @@ Definition of the JRSResume class.
return newObject;
};
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
@ -160,6 +230,8 @@ Definition of the JRSResume class.
});
};
/**
Determine if the sheet includes a specific skill.
*/
@ -172,11 +244,13 @@ Definition of the JRSResume class.
});
};
/**
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 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]
@ -190,6 +264,8 @@ Definition of the JRSResume class.
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().
@ -212,6 +288,8 @@ Definition of the JRSResume class.
return 0;
};
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
@ -238,6 +316,78 @@ Definition of the JRSResume class.
};
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
@ -269,9 +419,13 @@ Definition of the JRSResume class.
});
}
/**
Export the JRSResume function/ctor.
*/
module.exports = JRSResume;
}());

85
src/core/jrs-theme.js Normal file
View File

@ -0,0 +1,85 @@
/**
Definition of the JRSTheme class.
@module jrs-theme.js
@license MIT. See LICENSE.MD for details.
*/
(function() {
var _ = 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. See also: FRESHTheme.
@class JRSTheme
*/
function JRSTheme() {
}
/**
Open and parse the specified theme.
@method open
*/
JRSTheme.prototype.open = function( thFolder ) {
this.folder = thFolder;
// Open the [theme-name].json file; should have the same
// name as folder
var pathInfo = parsePath( thFolder );
// Open and parse the theme's package.json file.
var pkgJsonPath = PATH.join( thFolder, 'package.json' );
if( pathExists( pkgJsonPath )) {
var thApi = require( thFolder )
, thPkg = require( pkgJsonPath );
this.name = thPkg.name;
this.render = (thApi && thApi.render) || undefined;
this.formats = {
html: { title:'html', outFormat:'html', ext:'html' }
};
}
else {
throw { fluenterror: 10 };
}
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 ];
};
module.exports = JRSTheme;
}());

131
src/core/resume-factory.js Normal file
View File

@ -0,0 +1,131 @@
/**
Definition of the ResumeFactory class.
@license MIT. See LICENSE.md for details.
@module resume-factory.js
*/
(function(){
require('string.prototype.startswith');
var FS = require('fs');
var ResumeConverter = require('./convert');
var chalk = require('chalk');
var SyntaxErrorEx = require('../utils/syntax-error-ex');
/**
A simple factory class for FRESH and JSON Resumes.
@class ResumeFactory
*/
var ResumeFactory = module.exports = {
/**
Load one or more resumes from disk.
*/
load: function ( sources, opts ) {
// Loop over all inputs, parsing each to JSON and then to a FRESHResume
// or JRSResume object.
var that = this;
return sources.map( function( src ) {
return that.loadOne( src, opts );
});
},
/**
Load a single resume from disk.
*/
loadOne: function( src, opts ) {
var log = opts.log;
var toFormat = opts.format;
var objectify = opts.objectify;
// Get the destination format. Can be 'fresh', 'jrs', or null/undefined.
toFormat && (toFormat = toFormat.toLowerCase().trim());
// Load and parse the resume JSON
var info = _parse( src, opts );
if( info.error ) return info;
// Determine the resume format: FRESH or JRS
var json = info.json;
var orgFormat = ( json.meta && json.meta.format &&
json.meta.format.startsWith('FRESH@') ) ?
'fresh' : 'jrs';
// Convert between formats if necessary
if( toFormat && (orgFormat !== toFormat) ) {
json = ResumeConverter[ 'to' + toFormat.toUpperCase() ]( json );
}
// Objectify the resume, that is, convert it from JSON to a FRESHResume
// or JRSResume object.
var rez;
if( objectify ) {
var ResumeClass = require('../core/' + (toFormat || orgFormat) + '-resume');
rez = new ResumeClass().parseJSON( json );
rez.i().file = src;
}
return {
file: src,
json: info.json,
rez: rez
};
}
};
function _parse( fileName, opts ) {
var rawData;
try {
// TODO: Core should not log
opts.log( chalk.cyan('Reading resume: ') + chalk.cyan.bold(fileName) );
// Read the file
rawData = FS.readFileSync( fileName, 'utf8' );
// Parse it to JSON
return {
json: JSON.parse( rawData )
};
}
catch( ex ) {
// JSON.parse failed due to invalid JSON
if ( !opts.muffle && ex instanceof SyntaxError) {
var info = new SyntaxErrorEx( ex, rawData );
opts.log( chalk.red.bold(fileName.toUpperCase() + ' contains invalid JSON on line ' +
info.line + ' column ' + info.col + '.' +
chalk.red(' Unable to validate.')));
opts.log( chalk.red.bold('INTERNAL: ' + ex) );
ex.handled = true;
}
if( opts.throw ) throw ex;
else return {
error: ex,
raw: rawData,
file: fileName
};
}
}
}());

22
src/core/spawn-watch.js Normal file
View File

@ -0,0 +1,22 @@
/**
@module spawn-watch.js
*/
(function() {
// Catch various out-of-band child process errors such as ENOENT for PDFs
// http://stackoverflow.com/q/27688804
var SpawnWatcher = module.exports = function() {
var childProcess = require("child_process");
var oldSpawn = childProcess.spawn;
childProcess.spawn = function() {
return oldSpawn.apply(this, arguments)
.on('error', function(err) {
require('./error-handler').err( err, false );
});
};
}();
//SpawnWatcher();
}());

24
src/core/status-codes.js Normal file
View File

@ -0,0 +1,24 @@
/**
Status codes for HackMyResume.
@module status-codes.js
@license MIT. See LICENSE.MD for details.
*/
(function(){
module.exports = {
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3,
missingCommand: 4,
invalidCommand: 5,
resumeNotFoundAlt: 6,
inputOutputParity: 7,
createNameMissing: 8,
wkhtmltopdf: 9,
missingPackageJSON: 10,
invalid: 11
};
}());

290
src/eng/generic-helpers.js Normal file
View File

@ -0,0 +1,290 @@
/**
Generic template helper definitions for HackMyResume / FluentCV.
@license MIT. See LICENSE.md for details.
@module generic-helpers.js
*/
(function() {
var MD = require('marked')
, H2W = require('../utils/html-to-wpml')
, XML = require('xml-escape')
, moment = require('moment')
, LO = require('lodash')
, _ = require('underscore')
, unused = require('../utils/string');
/**
Generic template helper function definitions.
@class GenericHelpers
*/
var 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) {
if (moment) {
var momentDate = moment( datetime );
if (momentDate.isValid()) return momentDate.format(format);
}
return datetime || (typeof fallback == 'string' ? fallback : (fallback === true ? 'Present' : null));
},
/**
Format a from/to date range.
@method dateRange
*/
dateRange: function( obj, fmt, sep, options ) {
fmt = (fmt && String.is(fmt) && fmt) || 'YYYY-MM';
sep = (sep && String.is(sep) && sep) || ' — ';
if( obj.safe ) {
var dateA = (obj.safe.start && obj.safe.start.format(fmt)) || '';
var dateB = (obj.safe.end && obj.safe.end.format(fmt)) || '';
if( obj.safe.start && obj.safe.end ) {
return dateA + sep + dateB ;
}
else if( obj.safe.start || obj.safe.end ) {
return dateA || dateB;
}
}
return '';
},
/**
Return true if the section is present on the resume and has at least one
element.
@method section
*/
section: function( title, options ) {
title = title.trim().toLowerCase();
var obj = LO.get( this.r, title );
if( _.isArray( obj ) ) {
return obj.length ? options.fn(this) : undefined;
}
else if( _.isObject( obj )) {
return ( (obj.history && obj.history.length) ||
( obj.sets && obj.sets.length ) ) ?
options.fn(this) : undefined;
}
},
/**
Capitalize the first letter of the word.
@method section
*/
camelCase: function(val) {
val = (val && val.trim()) || '';
return val ? (val.charAt(0).toUpperCase() + val.slice(1)) : 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 ) {
// If not provided by the user, stitle should default to sname. ps.
// Handlebars silently passes in the options object to the last param,
// where in Underscore stitle will be null/undefined, so we check both.
stitle = (stitle && String.is(stitle) && stitle) || sname;
// If there's a section title override, use it.
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 ) {
return url && url.trim() ?
('<a href="' + url + '">' + text + '</a>') : text;
},
/**
Return the last word of the specified text.
@method lastWord
*/
lastWord: function( txt ) {
return txt && txt.trim() ? _.last( txt.split(' ') ) : '';
},
/**
Convert a skill level to an RGB color triplet. 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 = skillLevelToIndex( lvl );
var skillColors = (this.theme && this.theme.palette &&
this.theme.palette.skillLevels) ||
[ '#FFFFFF', '#5CB85C', '#F1C40F', '#428BCA', '#C00000' ];
return skillColors[idx];
},
/**
Return an appropriate height. TODO: refactor
@method lastWord
*/
skillHeight: function( lvl ) {
var idx = skillLevelToIndex( lvl );
return ['38.25', '30', '16', '8', '0'][idx];
},
/**
Return all but the last word of the input text.
@method initialWords
*/
initialWords: function( txt ) {
return txt && txt.trim() ? _.initial( txt.split(' ') ).join(' ') : '';
},
/**
Trim the protocol (http or https) from a URL/
@method trimURL
*/
trimURL: function( url ) {
return url && url.trim() ? url.trim().replace(/^https?:\/\//i, '') : '';
},
/**
Convert text to lowercase.
@method toLower
*/
toLower: function( txt ) {
return txt && txt.trim() ? txt.toLowerCase() : '';
},
/**
Return true if either value is truthy.
@method either
*/
either: function( lhs, rhs, options ) {
if (lhs || rhs) return options.fn(this);
},
/**
Conditional stylesheet link. Either display the link or embed the stylesheet
via <style></style> tag.
*/
styleSheet: function( file, options ) {
var styles = ( this.opts.css === 'link') ?
'<link href="' + file + '" rel="stylesheet" type="text/css">' :
'<style>' + this.cssInfo.data + '</style>';
if( this.opts.themeObj.inherits &&
this.opts.themeObj.inherits.html &&
this.format === 'html' ) {
styles += (this.opts.css === 'link') ?
'<link href="' + this.opts.themeObj.overrides.path + '" rel="stylesheet" type="text/css">' :
'<style>' + this.opts.themeObj.overrides.data + '</style>';
}
return styles;
},
/**
Perform a generic comparison.
See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates
@method compare
*/
compare: function(lvalue, rvalue, options) {
if (arguments.length < 3)
throw new Error("Handlerbars Helper 'compare' needs 2 parameters");
var operator = options.hash.operator || "==";
var operators = {
'==': function(l,r) { return l == r; },
'===': function(l,r) { return l === r; },
'!=': function(l,r) { return l != r; },
'<': function(l,r) { return l < r; },
'>': function(l,r) { return l > r; },
'<=': function(l,r) { return l <= r; },
'>=': function(l,r) { return l >= r; },
'typeof': function(l,r) { return typeof l == r; }
};
if (!operators[operator])
throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator);
var result = operators[operator](lvalue,rvalue);
return result ? options.fn(this) : options.inverse(this);
}
};
function skillLevelToIndex( lvl ) {
var idx = 0;
if( String.is( lvl ) ) {
lvl = lvl.trim().toLowerCase();
var intVal = parseInt( lvl );
if( isNaN( intVal ) ) {
switch( lvl ) {
case 'beginner': idx = 1; break;
case 'intermediate': idx = 2; break;
case 'advanced': idx = 3; break;
case 'master': idx = 4; break;
}
}
else {
idx = Math.min( intVal / 2, 4 );
idx = Math.max( 0, idx );
}
}
else {
idx = Math.min( lvl / 2, 4 );
idx = Math.max( 0, idx );
}
return idx;
}
}());

View File

@ -1,73 +1,99 @@
/**
Handlebars template generate for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
Definition of the HandlebarsGenerator class.
@license MIT. See LICENSE.md for details.
@module handlebars-generator.js
*/
(function() {
var _ = require('underscore');
var HANDLEBARS = require('handlebars');
var FS = require('fs');
var moment = require('moment');
var MD = require('marked');
var H2W = require('../utils/html-to-wpml');
module.exports = function( json, jst, format, cssInfo, opts, theme ) {
var _ = require('underscore')
, HANDLEBARS = require('handlebars')
, FS = require('fs')
, registerHelpers = require('./handlebars-helpers')
, PATH = require('path')
, parsePath = require('parse-filepath')
, READFILES = require('recursive-readdir-sync')
, SLASH = require('slash');
/**
Perform template-based resume generation using Handlebars.js.
@class HandlebarsGenerator
*/
var HandlebarsGenerator = module.exports = {
generate: function( json, jst, format, cssInfo, opts, theme ) {
registerPartials( format, theme );
registerHelpers( theme );
// Preprocess text
var encData = json;
( format === 'html' || format === 'pdf' ) && (encData = json.markdownify());
( format === 'doc' ) && (encData = json.xmlify());
// Compile and run the Handlebars template.
var template = HANDLEBARS.compile(jst);
return template({
r: encData,
RAW: json,
filt: opts.filters,
cssInfo: cssInfo,
format: format,
opts: opts,
headFragment: opts.headFragment || ''
});
}
};
function registerPartials(format, theme) {
if( format !== 'html' && format != 'doc' )
return;
// Locate the global partials folder
var partialsFolder = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/partials/',
format
);
// Register global partials in the /partials folder
// TODO: Only do this once per HMR invocation.
_.each( READFILES( partialsFolder, function(error){ }), function( el ) {
var pathInfo = parsePath( el );
var name = SLASH( PATH.relative( partialsFolder, el )
.replace(/\.html$|\.xml$/, '') );
if( pathInfo.dirname.endsWith('section') ) {
name = SLASH(name.replace(/\.html$|\.xml$/, ''));
}
var tplData = FS.readFileSync( el, 'utf8' );
var compiledTemplate = HANDLEBARS.compile( tplData );
HANDLEBARS.registerPartial( name, compiledTemplate );
theme.partialsInitialized = true;
});
// Register theme-specific partials
_.each( theme.partials, function( el ) {
var tplData = FS.readFileSync( el.path, 'utf8' );
var compiledTemplate = HANDLEBARS.compile( tplData );
HANDLEBARS.registerPartial( el.name, compiledTemplate );
});
}
HANDLEBARS.registerHelper("formatDate", function(datetime, format) {
if( moment ) {
return moment( datetime ).format( format );
}
else {
return datetime;
}
});
HANDLEBARS.registerHelper("wpml", function( txt, inline ) {
inline = (inline && !inline.hash) || false;
txt = inline ? MD(txt.trim()).replace(/^\s*<p>|<\/p>\s*$/gi, '') : MD(txt.trim());
txt = H2W( txt.trim() );
return txt;
});
// http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates/
HANDLEBARS.registerHelper('compare', function(lvalue, rvalue, options) {
if (arguments.length < 3)
throw new Error("Handlerbars Helper 'compare' needs 2 parameters");
var operator = options.hash.operator || "==";
var operators = {
'==': function(l,r) { return l == r; },
'===': function(l,r) { return l === r; },
'!=': function(l,r) { return l != r; },
'<': function(l,r) { return l < r; },
'>': function(l,r) { return l > r; },
'<=': function(l,r) { return l <= r; },
'>=': function(l,r) { return l >= r; },
'typeof': function(l,r) { return typeof l == r; }
};
if (!operators[operator])
throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator);
var result = operators[operator](lvalue,rvalue);
return result ? options.fn(this) : options.inverse(this);
});
var template = HANDLEBARS.compile(jst);
return template({
r: json,
filt: opts.filters,
cssInfo: cssInfo,
headFragment: opts.headFragment || ''
});
};
}());

View File

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

View File

@ -1,37 +1,57 @@
/**
Underscore template generate for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
Definition of the UnderscoreGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module underscore-generator.js
*/
(function() {
var _ = require('underscore');
module.exports = function( json, jst, format, cssInfo, opts, theme ) {
// Tweak underscore's default template delimeters
var delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template;
if( opts.themeObj && opts.themeObj.delimeters ) {
delims = _.mapObject( delims, function(val,key) {
return new RegExp( val, "ig");
/**
Perform template-based resume generation using Underscore.js.
@class UnderscoreGenerator
*/
var UnderscoreGenerator = module.exports = {
generate: function( json, jst, format, cssInfo, opts, theme ) {
// Tweak underscore's default template delimeters
var delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template;
if( opts.themeObj && opts.themeObj.delimeters ) {
delims = _.mapObject( delims, function(val,key) {
return new RegExp( val, "ig");
});
}
_.templateSettings = delims;
// Strip {# comments #}
jst = jst.replace( delims.comment, '');
var helpers = require('./generic-helpers');
helpers.opts = opts;
helpers.cssInfo = cssInfo;
// Compile and run the template. TODO: avoid unnecessary recompiles.
var compiled = _.template(jst);
var ret = compiled({
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,
h: helpers
});
return ret;
}
_.templateSettings = delims;
// Strip {# comments #}
jst = jst.replace( delims.comment, '');
// Compile and run the template. TODO: avoid unnecessary recompiles.
var compiled = _.template(jst);
var ret = compiled({
r: format === 'html' || format === 'pdf' ? json.markdownify() : json,
filt: opts.filters,
XML: require('xml-escape'),
RAW: json,
cssInfo: cssInfo,
headFragment: opts.headFragment || ''
});
return ret;
};
}());

View File

@ -1,348 +0,0 @@
/**
Internal resume generation logic for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
@module fluentcmd.js
*/
(function() {
module.exports = function () {
var path = require( 'path' )
, extend = require( './utils/extend' )
, unused = require('./utils/string')
, FS = require('fs')
, _ = require('underscore')
, FLUENT = require('./fluentlib')
, PATH = require('path')
, MKDIRP = require('mkdirp')
//, COLORS = require('colors')
, rez, _log, _err;
/**
Given a source JSON resume, a destination resume path, and a theme file,
generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
*/
function generate( src, dst, opts, logger, errHandler ) {
_log = logger || console.log;
_err = errHandler || error;
//_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
// Load input resumes...
if(!src || !src.length) { throw { fluenterror: 3 }; }
var sheets = loadSourceResumes( src );
// Merge input resumes...
var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ?
'Merging '.gray+ a.imp.fileName : '') + ' onto '.gray + b.imp.fileName;
return extend( true, b, a );
});
msg && _log(msg);
// Verify the specified theme name/path
var relativeThemeFolder = '../node_modules/fluent-themes/themes';
var tFolder = PATH.resolve( __dirname, relativeThemeFolder, _opts.theme);
var exists = require('./utils/file-exists');
if (!exists( tFolder )) {
tFolder = PATH.resolve( _opts.theme );
if (!exists( tFolder )) {
throw { fluenterror: 1, data: _opts.theme };
}
}
// Load the theme
var theTheme = new FLUENT.Theme().open( tFolder );
_opts.themeObj = theTheme;
_log( 'Applying '.info + theTheme.name.toUpperCase().infoBold +
(' theme (' +Object.keys(theTheme.formats).length + ' formats)').info);
// Expand output resumes... (can't use map() here)
var targets = [], that = this;
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) {
var to = path.resolve(t),
pa = path.parse(to),
fmat = pa.ext || '.all';
targets.push.apply(targets, fmat === '.all' ?
Object.keys( theTheme.formats ).map(function(k){
var z = theTheme.formats[k];
return { file: to.replace(/all$/g,z.outFormat), fmt: z };
}) : [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
});
// Run the transformation!
var finished = targets.map( function(t) { return single(t, theTheme); });
// Don't send the client back empty-handed
return { sheet: rez, targets: targets, processed: finished };
}
/**
Generate a single resume of a specific format.
@param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function single( targInfo, theme ) {
try {
var f = targInfo.file
, fType = targInfo.fmt.outFormat
, fName = path.basename(f, '.' + fType)
, theFormat;
// If targInfo.fmt.files exists, this theme has an explicit "files"
// section in its theme.json file.
if( targInfo.fmt.files && targInfo.fmt.files.length ) {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + path.relative(process.cwd(), f ).useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
// targInfo.fmt.files.forEach( function( form ) {
//
// if( form.action === 'transform' ) {
// var theFormat = _fmts.filter( function( fmt ) {
// return fmt.name === targInfo.fmt.outFormat;
// })[0];
// MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
// theFormat.gen.generate( rez, f, _opts );
// }
// else if( form.action === null ) {
// // Copy the file
// }
//
// });
}
// Otherwise the theme has no files section
else {
_log( 'Generating '.useful +
targInfo.fmt.outFormat.toUpperCase().useful.bold +
' resume: '.useful + path.relative(process.cwd(), f ).useful.bold);
theFormat = _fmts.filter(
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0];
MKDIRP.sync( path.dirname( f ) ); // Ensure dest folder exists;
theFormat.gen.generate( rez, f, _opts );
}
}
catch( ex ) {
_err( ex );
}
}
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
/**
Validate 1 to N resumes in either FRESH or JSON Resume format.
*/
function validate( src, unused, opts, logger ) {
_log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
var isValid = true;
var validator = require('is-my-json-valid');
var schemas = {
fresh: require('FRESCA'),
jars: require('./core/resume.json')
};
// Load input resumes...
var sheets = loadSourceResumes(src, function( res ) {
try {
return {
file: res,
raw: FS.readFileSync( res, 'utf8' )
};
}
catch( ex ) {
throw ex;
}
});
sheets.forEach( function( rep ) {
var rez;
try {
rez = JSON.parse( rep.raw );
}
catch( ex ) {
_log('Validating '.info + rep.file.infoBold +
' against FRESH/JRS schema: '.info + 'ERROR!'.error.bold);
if (ex instanceof SyntaxError) {
// Invalid JSON
_log( '--> '.bold.red + rep.file.toUpperCase().red +
' contains invalid JSON. Unable to validate.'.red );
_log( (' INTERNAL: ' + ex).red );
}
else {
_log(('ERROR: ' + ex.toString()).red.bold);
}
return;
}
var isValid = false;
var style = 'useful';
var errors = [];
var fmt = rez.meta &&
(rez.meta.format === 'FRESH@0.1.0') ? 'fresh':'jars';
try {
var validate = validator( schemas[ fmt ], { // Note [1]
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
isValid = validate( rez );
if( !isValid ) {
style = 'warn';
errors = validate.errors;
}
}
catch(ex) {
}
_log( 'Validating '.info + rep.file.infoBold + ' against '.info +
fmt.replace('jars','JSON Resume').toUpperCase().infoBold +
' schema: '.info + (isValid ? 'VALID!' : 'INVALID')[style].bold );
errors.forEach(function(err,idx) {
_log( '--> '.bold.yellow +
(err.field.replace('data.','resume.').toUpperCase() + ' ' +
err.message).yellow );
});
});
}
/**
Convert between FRESH and JRS formats.
*/
function convert( src, dst, opts, logger ) {
_log = logger || console.log;
if( !src || !src.length ) { throw { fluenterror: 6 }; }
if( !dst || !dst.length ) {
if( src.length === 1 ) { throw { fluenterror: 5 }; }
else if( src.length === 2 ) { dst = [ src[1] ]; src = [ src[0] ]; }
else { throw { fluenterror: 5 }; }
}
if( src && dst && src.length && dst.length && src.length !== dst.length ) {
throw { fluenterror: 7 };
}
var sheets = loadSourceResumes( src );
sheets.forEach(function(sheet, idx){
var sourceFormat = sheet.imp.orgFormat === 'JRS' ? 'JRS' : 'FRESH';
var targetFormat = sourceFormat === 'JRS' ? 'FRESH' : 'JRS';
_log( 'Converting '.useful + sheet.imp.fileName.useful.bold + (' (' +
sourceFormat + ') to ').useful + dst[0].useful.bold +
(' (' + targetFormat + ').').useful );
sheet.saveAs( dst[idx], targetFormat );
});
}
/**
Create a new empty resume in either FRESH or JRS format.
*/
function create( src, dst, opts, logger ) {
_log = logger || console.log;
dst = src || ['resume.json'];
dst.forEach( function( t ) {
var safeFormat = opts.format.toUpperCase();
_log('Creating new '.useful +safeFormat.useful.bold +
' resume: '.useful + t.useful.bold);
MKDIRP.sync( path.dirname( t ) ); // Ensure dest folder exists;
FLUENT[ safeFormat + 'Resume' ].default().save( t );
});
}
/**
Display help documentation.
*/
function help() {
console.log( FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' )
.useful.bold );
}
function loadSourceResumes( src, fn ) {
return src.map( function( res ) {
_log( 'Reading '.info + 'SOURCE'.infoBold + ' resume: '.info +
res.cyan.bold );
return (fn && fn(res)) || (new FLUENT.FRESHResume()).open( res );
});
}
/**
Supported resume formats.
*/
var _fmts = [
{ name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() },
{ name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() },
{ name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new FLUENT.LaTeXGenerator() }
];
/**
Default FluentCV options.
*/
var _opts = {
theme: 'modern',
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
unformatted: ['em','strong'],
max_char: 80, // ← See lib/html.js in above-linked repo
//wrap_line_length: 120, ← Don't use this
}
};
/**
Internal module interface. Used by FCV Desktop and HMR.
*/
return {
verbs: {
build: generate,
validate: validate,
convert: convert,
new: create,
help: help
},
lib: require('./fluentlib'),
options: _opts,
formats: _fmts
};
}();
}());
// [1]: JSON.parse throws SyntaxError on invalid JSON. See:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse

View File

@ -1,21 +0,0 @@
/**
External API surface for FluentCV:CLI.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk
*/
module.exports = {
Sheet: require('./core/fresh-resume'),
FRESHResume: require('./core/fresh-resume'),
JRSResume: require('./core/jrs-resume'),
Theme: require('./core/theme'),
FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./gen/html-generator'),
TextGenerator: require('./gen/text-generator'),
HtmlPdfGenerator: require('./gen/html-pdf-generator'),
WordGenerator: require('./gen/word-generator'),
MarkdownGenerator: require('./gen/markdown-generator'),
JsonGenerator: require('./gen/json-generator'),
YamlGenerator: require('./gen/yaml-generator'),
JsonYamlGenerator: require('./gen/json-yaml-generator'),
LaTeXGenerator: require('./gen/latex-generator')
};

View File

@ -1,6 +1,7 @@
/**
Base resume generator for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
Definition of the BaseGenerator class.
@module base-generator.js
@license MIT. See LICENSE.md for details.
*/
(function() {
@ -25,14 +26,7 @@ Base resume generator for FluentCV.
/**
Status codes.
*/
codes: {
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3,
missingCommand: 4,
invalidCommand: 5
},
codes: require('../core/status-codes'),
/**
Generator options.

View File

@ -1,6 +1,7 @@
/**
HTML resume generator for FluentCV.
@license Copyright (c) 2015 James M. Devlin / FluentDesk
Definition of the HTMLGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module html-generator.js
*/
(function() {
@ -21,16 +22,6 @@ HTML resume generator for FluentCV.
the HTML resume prior to saving.
*/
onBeforeSave: function( info ) {
var cssSrc = PATH.join( info.theme.folder, 'src', '*.css' )
, outFolder = PATH.parse( info.outputFile ).dir, that = this;
info.theme.cssFiles.forEach( function( f ) {
var fi = PATH.parse( f.path );
FS.copySync( f.path, PATH.join( outFolder, fi.base ), { clobber: true }, function( e ) {
throw { fluenterror: that.codes.copyCss, data: [cssSrc,cssDst] };
});
});
return this.opts.prettify ?
HTML.prettyPrint( info.mk, this.opts.prettify ) : info.mk;
}

View File

@ -1,6 +1,7 @@
/**
Definition of the HtmlPdfGenerator class.
@license Copyright (c) 2015 James M. Devlin / FluentDesk
@module html-pdf-generator.js
@license MIT. See LICENSE.md for details.
*/
(function() {
@ -10,7 +11,7 @@ Definition of the HtmlPdfGenerator class.
, HTML = require( 'html' );
/**
An HTML-based PDF resume generator for FluentCV.
An HTML-driven PDF resume generator for HackMyResume.
*/
var HtmlPdfGenerator = module.exports = TemplateGenerator.extend({
@ -22,20 +23,36 @@ Definition of the HtmlPdfGenerator class.
Generate the binary PDF.
*/
onBeforeSave: function( info ) {
pdf(info.mk, info.outputFile);
return info.mk;
engines[ info.opts.pdf || 'wkhtmltopdf' ]
.call( this, info.mk, info.outputFile );
return null; // halt further processing
}
});
/**
Generate a PDF from HTML.
*/
function pdf( markup, fOut ) {
var pdfCount = 0;
if( false ) { //( _opts.pdf === 'phantom' || _opts.pdf == 'all' ) {
pdfCount++;
var engines = {
/**
Generate a PDF from HTML using wkhtmltopdf.
*/
wkhtmltopdf: function(markup, fOut) {
var wk;
try {
wk = require('wkhtmltopdf');
wk( markup, { pageSize: 'letter' } )
.pipe( FS.createWriteStream( fOut ) );
}
catch(ex) {
// { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', ... }
// { [Error: ENOENT] }
throw { fluenterror: this.codes.wkhtmltopdf };
}
},
/**
Generate a PDF from HTML using Phantom.
*/
phantom: function( markup, fOut ) {
require('phantom').create( function( ph ) {
ph.createPage( function( page ) {
page.setContent( markup );
@ -50,22 +67,15 @@ Definition of the HtmlPdfGenerator class.
});
page.set('onLoadFinished', function(success) {
page.render( fOut );
pdfCount++;
ph.exit();
});
},
{ dnodeOpts: { weak: false } } );
});
}
if( true ) { // _opts.pdf === 'wkhtmltopdf' || _opts.pdf == 'all' ) {
var fOut2 = fOut;
if( pdfCount == 1 ) {
fOut2 = fOut2.replace(/\.pdf$/g, '.b.pdf');
}
require('wkhtmltopdf')( markup, { pageSize: 'letter' } )
.pipe( FS.createWriteStream( fOut2 ) );
pdfCount++;
}
}
};
}());

View File

@ -0,0 +1,47 @@
/**
Definition of the HtmlPngGenerator class.
@license MIT. See LICENSE.MD for details.
@module html-png-generator.js
*/
(function() {
var TemplateGenerator = require('./template-generator')
, FS = require('fs-extra')
, HTML = require( 'html' );
/**
An HTML-based PNG resume generator for HackMyResume.
*/
var HtmlPngGenerator = module.exports = TemplateGenerator.extend({
init: function() {
this._super( 'png', 'html' );
},
invoke: function( rez, themeMarkup, cssInfo, opts ) {
//return YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 );
},
generate: function( rez, f, opts ) {
var htmlResults = opts.targets.filter(function(t){
return t.fmt.outFormat === 'html';
});
var htmlFile = htmlResults[0].final.files.filter(function(fl){
return fl.info.ext === 'html';
});
png(htmlFile[0].data, f);
}
});
/**
Generate a PNG from HTML.
*/
function png( markup, fOut ) {
// require('webshot')( markup , { encoding: 'binary', siteType: 'html' } )
// .pipe( FS.createWriteStream( fOut ) );
require('webshot')( markup , fOut, { siteType: 'html' }, function(err) { } );
}
}());

View File

@ -1,6 +1,7 @@
/**
Definition of the JsonGenerator class.
@license Copyright (c) 2015 | James M. Devlin
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module json-generator.js
*/
var BaseGenerator = require('./base-generator');

View File

@ -1,7 +1,7 @@
/**
A JSON-driven YAML resume generator for FluentLib.
Definition of the JsonYamlGenerator class.
@module json-yaml-generator.js
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
*/
(function() {

View File

@ -1,6 +1,7 @@
/**
LaTeX resume generator for FluentCV.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
Definition of the LaTeXGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module latex-generator.js
*/
var TemplateGenerator = require('./template-generator');

View File

@ -1,6 +1,7 @@
/**
Markdown resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the MarkdownGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module markdown-generator.js
*/
var TemplateGenerator = require('./template-generator');

View File

@ -1,6 +1,7 @@
/**
Template-based resume generator base for FluentCV.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
Definition of the TemplateGenerator class.
@license MIT. See LICENSE.md for details.
@module template-generator.js
*/
(function() {
@ -12,10 +13,12 @@ Template-based resume generator base for FluentCV.
, MD = require( 'marked' )
, XML = require( 'xml-escape' )
, PATH = require('path')
, parsePath = require('parse-filepath')
, MKDIRP = require('mkdirp')
, BaseGenerator = require( './base-generator' )
, EXTEND = require('../utils/extend')
, Theme = require('../core/theme');
, FRESHTheme = require('../core/fresh-theme')
, JRSTheme = require('../core/jrs-theme');
@ -37,7 +40,9 @@ Template-based resume generator base for FluentCV.
raw: function( txt ) { return txt; },
xml: function( txt ) { return XML(txt); },
md: function( txt ) { return MD( txt || '' ); },
mdin: function( txt ) { return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); },
mdin: function( txt ) {
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
},
lower: function( txt ) { return txt.toLowerCase(); },
link: function( name, url ) { return url ?
'<a href="' + url + '">' + name + '</a>' : name; }
@ -68,24 +73,21 @@ Template-based resume generator base for FluentCV.
},
invoke: function( rez, themeMarkup, cssInfo, opts ) {
this.opts = EXTEND( true, {}, _defaultOpts, opts );
mk = this.single( rez, themeMarkup, this.format, cssInfo, { } );
this.onBeforeSave && (mk = this.onBeforeSave( mk, themeFile, f ));
return mk;
},
/**
Default generation method for template-based generators.
@method generate
String-based template generation method.
@method invoke
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options.
@returns An array of objects representing the generated output files. Each
object has this format:
{
files: [ { info: { }, data: [ ] }, { ... } ],
themeInfo: { }
}
*/
generate: function( rez, f, opts ) {
invoke: function( rez, opts ) {
// Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts );
@ -95,20 +97,82 @@ Template-based resume generator base for FluentCV.
var theme = themeInfo.theme;
var tFolder = themeInfo.folder;
var tplFolder = PATH.join( tFolder, 'src' );
var outFolder = PATH.parse(f).dir;
var curFmt = theme.getFormat( this.format );
var that = this;
// "Generate": process individual files within the theme
curFmt.files.forEach(function(tplInfo){
if( tplInfo.action === 'transform' ) {
transform.call( that, rez, f, tplInfo, theme, outFolder );
return {
files: curFmt.files.map( function( tplInfo ) {
return {
info: tplInfo,
data: tplInfo.action === 'transform' ?
transform.call( that, rez, tplInfo, theme ) : undefined
};
}).filter(function(item){ return item !== null; }),
themeInfo: themeInfo
};
},
/**
File-based template generation method.
@method generate
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options.
*/
generate: function( rez, f, opts ) {
// Call the generation method
var genInfo = this.invoke( rez, opts );
// Carry over options
this.opts = EXTEND( true, { }, _defaultOpts, opts );
// Load the theme
var themeInfo = genInfo.themeInfo;
var theme = themeInfo.theme;
var tFolder = themeInfo.folder;
var tplFolder = PATH.join( tFolder, 'src' );
var outFolder = parsePath(f).dirname;
var curFmt = theme.getFormat( this.format );
var that = this;
// "Generate": process individual files within the theme
genInfo.files.forEach(function( file ){
var thisFilePath;
if( file.info.action === 'transform' ) {
thisFilePath = PATH.join( outFolder, file.info.orgPath );
try {
if( that.onBeforeSave ) {
file.data = that.onBeforeSave({
theme: theme,
outputFile: (file.info.major ? f : thisFilePath),
mk: file.data,
opts: that.opts
});
if( !file.data ) return; // PDF etc
}
var fileName = file.info.major ? f : thisFilePath;
MKDIRP.sync( PATH.dirname( fileName ) );
FS.writeFileSync( fileName, file.data,
{ encoding: 'utf8', flags: 'w' } );
that.onAfterSave && that.onAfterSave(
{ outputFile: fileName, mk: file.data, opts: that.opts } );
}
catch(ex) {
require('../core/error-handler').err(ex, false);
}
}
else if( tplInfo.action === null && theme.explicit ) {
var thisFilePath = PATH.join(outFolder, tplInfo.orgPath);
else if( file.info.action === null/* && theme.explicit*/ ) {
thisFilePath = PATH.join( outFolder, file.info.orgPath );
try {
MKDIRP.sync( PATH.dirname(thisFilePath) );
FS.copySync( tplInfo.path, thisFilePath );
FS.copySync( file.info.path, thisFilePath );
}
catch(ex) {
console.log(ex);
@ -122,11 +186,13 @@ Template-based resume generator base for FluentCV.
var absLoc = PATH.join(outFolder, loc);
var absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
// 'file', 'dir', or 'junction' (Windows only)
var type = PATH.parse( absLoc ).ext ? 'file' : 'junction';
var type = parsePath( absLoc ).extname ? 'file' : 'junction';
FS.symlinkSync( absTarg, absLoc, type);
});
}
return genInfo;
},
@ -141,9 +207,10 @@ Template-based resume generator base for FluentCV.
*/
single: function( json, jst, format, cssInfo, opts, theme ) {
this.opts.freezeBreaks && ( jst = freeze(jst) );
var eng = require( '../eng/' + ((opts.themeObj && opts.themeObj.engine) ||
opts.engine) + '-generator' );
var result = eng( json, jst, format, cssInfo, opts, theme );
var eng = require( '../eng/' + theme.engine + '-generator' );
var result = eng.generate( json, jst, format, cssInfo, opts, theme );
this.opts.freezeBreaks && ( result = unfreeze(result) );
return result;
}
@ -164,20 +231,28 @@ Template-based resume generator base for FluentCV.
Given a theme title, load the corresponding theme.
*/
function themeFromMoniker() {
// Verify the specified theme name/path
var tFolder = PATH.join(
PATH.parse( require.resolve('fluent-themes') ).dir,
parsePath( require.resolve('fresh-themes') ).dirname,
'/themes/',
this.opts.theme
);
var exists = require('../utils/file-exists');
if( !exists( tFolder ) ) {
tFolder = PATH.resolve( this.opts.theme );
if( !exists( tFolder ) ) {
throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme};
}
}
var t = this.opts.themeObj || new Theme().open( tFolder );
var t;
if( this.opts.theme.startsWith('jsonresume-theme-') ) {
t = new JRSTheme().open( tFolder );
}
else {
var exists = require('path-exists').sync;
if( !exists( tFolder ) ) {
tFolder = PATH.resolve( this.opts.theme );
if( !exists( tFolder ) ) {
throw { fluenterror: this.codes.themeNotFound, data: this.opts.theme};
}
}
t = this.opts.themeObj || new FRESHTheme().open( tFolder );
}
// Load the theme and format
return {
@ -188,21 +263,19 @@ Template-based resume generator base for FluentCV.
/**
Transform a single subfile.
*/
function transform( rez, f, tplInfo, theme, outFolder ) {
var cssInfo = { file: tplInfo.css ? tplInfo.cssPath : null, data: tplInfo.css || null };
var mk = this.single( rez, tplInfo.data, this.format, cssInfo, this.opts, theme );
this.onBeforeSave && (mk = this.onBeforeSave( { mk: mk, theme: theme, outputFile: f } ));
var thisFilePath = PATH.join( outFolder, tplInfo.orgPath );
function transform( rez, tplInfo, theme ) {
try {
MKDIRP.sync( PATH.dirname( tplInfo.major ? f : thisFilePath) );
FS.writeFileSync( tplInfo.major ? f : thisFilePath, mk, { encoding: 'utf8', flags: 'w' } );
this.onAfterSave && (mk = this.onAfterSave( { outputFile: (tplInfo.major ? f : thisFilePath), mk: mk } ));
var cssInfo = {
file: tplInfo.css ? tplInfo.cssPath : null,
data: tplInfo.css || null
};
return this.single( rez, tplInfo.data, this.format, cssInfo, this.opts,
theme );
}
catch(ex) {
console.log(ex);
throw ex;
}
}

View File

@ -1,6 +1,7 @@
/**
Plain text resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the TextGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module text-generator.js
*/
var TemplateGenerator = require('./template-generator');

View File

@ -1,6 +1,7 @@
/**
MS Word resume generator for FluentCV.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the WordGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module word-generator.js
*/
(function() {

View File

@ -1,6 +1,7 @@
/**
XML resume generator for FluentCV.
@license Copyright (c) 2015 | James M. Devlin
Definition of the XMLGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module xml-generator.js
*/
var BaseGenerator = require('./base-generator');
@ -8,7 +9,7 @@ var BaseGenerator = require('./base-generator');
/**
The XmlGenerator generates an XML resume via the TemplateGenerator.
*/
var XmlGenerator = module.exports = BaseGenerator.extend({
var XMLGenerator = module.exports = BaseGenerator.extend({
init: function(){
this._super( 'xml' );

View File

@ -1,7 +1,7 @@
/**
A YAML resume generator for FluentLib.
Definition of the YAMLGenerator class.
@module yaml-generator.js
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
*/
@ -13,7 +13,7 @@ A YAML resume generator for FluentLib.
YamlGenerator generates a YAML-formatted resume via TemplateGenerator.
*/
var YamlGenerator = module.exports = TemplateGenerator.extend({
var YAMLGenerator = module.exports = TemplateGenerator.extend({
init: function(){
this._super( 'yml', 'yml' );

43
src/hackmyapi.js Normal file
View File

@ -0,0 +1,43 @@
/**
External API surface for HackMyResume.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
@module hackmyapi.js
*/
(function() {
var v = {
build: require('./verbs/generate'),
analyze: require('./verbs/analyze'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
new: require('./verbs/create')
};
var HackMyAPI = module.exports = {
verbs: v,
alias: {
generate: v.build,
create: v.new
},
options: require('./core/default-options'),
formats: require('./core/default-formats'),
Sheet: require('./core/fresh-resume'),
FRESHResume: require('./core/fresh-resume'),
JRSResume: require('./core/jrs-resume'),
FRESHTheme: require('./core/fresh-theme'),
JRSTheme: require('./core/jrs-theme'),
FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./gen/html-generator'),
TextGenerator: require('./gen/text-generator'),
HtmlPdfGenerator: require('./gen/html-pdf-generator'),
WordGenerator: require('./gen/word-generator'),
MarkdownGenerator: require('./gen/markdown-generator'),
JsonGenerator: require('./gen/json-generator'),
YamlGenerator: require('./gen/yaml-generator'),
JsonYamlGenerator: require('./gen/json-yaml-generator'),
LaTeXGenerator: require('./gen/latex-generator'),
HtmlPngGenerator: require('./gen/html-png-generator')
};
}());

View File

@ -1,19 +1,29 @@
#! /usr/bin/env node
/**
Command-line interface (CLI) for FluentCV:CLI.
@license MIT. Copyright (c) 2015 James M. Devlin / FluentDesk.
Command-line interface (CLI) for HackMyResume.
@license MIT. Copyright (c) 2015 hacksalot (https://github.com/hacksalot)
@module index.js
*/
var ARGS = require( 'minimist' )
, FCMD = require( './fluentcmd')
var SPAWNW = require('./core/spawn-watch')
, HMR = require( './hackmyapi')
, PKG = require('../package.json')
, COLORS = require('colors')
, FS = require('fs')
, EXTEND = require('./utils/extend')
, chalk = require('chalk')
, PATH = require('path')
, opts = { }
, title = ('*** FluentCV v' + PKG.version + ' ***').bold.white
, _ = require('underscore');
, HACKMYSTATUS = require('./core/status-codes')
, safeLoadJSON = require('./utils/safe-json-loader')
, _opts = { }
, title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***')
, StringUtils = require('./utils/string.js')
, _ = require('underscore')
, Command = require('commander').Command;
@ -21,110 +31,212 @@ try {
main();
}
catch( ex ) {
handleError( ex );
require('./core/error-handler').err( ex, true );
}
/**
Kick off the HackMyResume application.
*/
function main() {
// Colorize
COLORS.setTheme({
title: ['white','bold'],
info: process.platform === 'win32' ? 'gray' : ['white','dim'],
infoBold: ['white','dim'],
warn: 'yellow',
error: 'red',
guide: 'yellow',
status: 'gray',//['white','dim'],
useful: 'green',
});
var args = initialize();
// Create the top-level (application) command...
var program = new Command('hackmyresume')
.version(PKG.version)
.description(chalk.yellow.bold('*** HackMyResume ***'))
.option('-o --opts <optionsFile>', 'Path to a .hackmyrc options file')
.option('-s --silent', 'Run in silent mode')
.option('--no-color', 'Disable colors')
.option('--color', 'Enable colors');
//.usage('COMMAND <sources> [TO <targets>]');
// Create the NEW command
program
.command('new')
.arguments('<sources...>')
.option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
.alias('create')
.description('Create resume(s) in FRESH or JSON RESUME format.')
.action(function( sources ) {
execVerb.call( this, sources, [], this.opts(), logMsg);
});
// Create the VALIDATE command
program
.command('validate')
.arguments('<sources...>')
.option('-a --assert', 'Treat validation warnings as errors', false)
.description('Validate a resume in FRESH or JSON RESUME format.')
.action(function(sources) {
execVerb.call( this, sources, [], this.opts(), logMsg);
});
// Create the CONVERT command
program
.command('convert')
//.arguments('<sources...>')
.description('Convert a resume to/from FRESH or JSON RESUME format.')
.action(function() {
var x = splitSrcDest.call( this );
execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
});
// Create the ANALYZE command
program
.command('analyze')
.arguments('<sources...>')
.description('Analyze one or more resumes.')
.action(function( sources ) {
execVerb.call( this, sources, [], this.opts(), logMsg);
});
// Create the BUILD command
program
.command('build')
.alias('generate')
//.arguments('<sources> TO [targets]')
//.usage('...')
.option('-t --theme <theme>', 'Theme name or path')
.option('-n --no-prettify', 'Disable HTML prettification', true)
.option('-c --css <option>', 'CSS linking / embedding', 'embed')
.option('-p --pdf <engine>', 'PDF generation engine')
.option('--no-tips', 'Disable theme tips and warnings.', false)
.description('Generate resume to multiple formats')
.action(function( sources, targets, options ) {
var x = splitSrcDest.call( this );
execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
});
// program.on('--help', function(){
// console.log(' Examples:');
// console.log('');
// console.log(' $ custom-help --help');
// console.log(' $ custom-help -h');
// console.log('');
// });
program.parse( args );
if (!program.args.length) { throw { fluenterror: 4 }; }
}
/**
Massage command-line args and setup Commander.js.
*/
function initialize() {
// Setup
if( process.argv.length <= 2 ) { throw { fluenterror: 4 }; }
var a = ARGS( process.argv.slice(2) );
opts = getOpts( a );
logMsg( title );
// Support case-insensitive sub-commands (build, generate, validate, etc.)..
var oVerb, verb = '', args = process.argv.slice(), cleanArgs = args.slice(2);
if( cleanArgs.length ) {
var verbIdx = _.findIndex( cleanArgs, function(v){ return v[0] !== '-'; });
if( verbIdx !== -1 ) {
oVerb = cleanArgs[ verbIdx ];
verb = args[ verbIdx + 2 ] = oVerb.trim().toLowerCase();
}
}
// Get the action to be performed
var params = a._.map( function(p){ return p.toLowerCase().trim(); });
var verb = params[0];
if( !FCMD.verbs[ verb ] ) {
logMsg('Invalid command: "'.warn + verb.warn.bold + '"'.warn);
// Handle invalid verbs here (a bit easier here than in commander.js)...
if( verb && !HMR.verbs[ verb ] && !HMR.alias[ verb ] ) {
throw { fluenterror: HACKMYSTATUS.invalidCommand, shouldExit: true,
attempted: oVerb };
}
// Override the .missingArgument behavior
Command.prototype.missingArgument = function(name) {
if( this.name() !== 'new' )
throw { fluenterror: HACKMYSTATUS.resumeNotFound };
};
// Override the .helpInformation behavior
Command.prototype.helpInformation = function() {
var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' );
return chalk.green.bold(manPage);
};
return args;
}
/**
Invoke a HackMyResume verb.
*/
function execVerb( src, dst, opts, log ) {
loadOptions.call( this, opts );
HMR.verbs[ this.name() ].call( null, src, dst, _opts, log );
}
/**
Initialize HackMyResume options.
*/
function loadOptions( opts ) {
opts.opts = this.parent.opts;
// Load the specified options file (if any) and apply options
if( opts.opts && String.is( opts.opts )) {
var json = safeLoadJSON( PATH.relative( process.cwd(), opts.opts ) );
json && ( opts = EXTEND( true, opts, json ) );
if( !json ) {
throw safeLoadJSON.error;
}
}
// Merge in command-line options
opts = EXTEND( true, opts, this.opts() );
opts.silent = this.parent.silent;
_opts = opts;
}
/**
Split multiple command-line filenames by the 'TO' keyword
*/
function splitSrcDest() {
var params = this.parent.args.filter(function(j) { return String.is(j); });
if( params.length === 0 )
throw { fluenterror: HACKMYSTATUS.resumeNotFound };
// Find the TO keyword, if any
var splitAt = _.findIndex( params, function(p) {
return p.toLowerCase() === 'to';
});
// TO can't be the last keyword
if( splitAt === params.length - 1 && splitAt !== -1 ) {
logMsg(chalk.yellow('Please ') +
chalk.yellow.bold('specify an output file') +
chalk.yellow(' for this operation or ') +
chalk.yellow.bold('omit the TO keyword') +
chalk.yellow('.') );
return;
}
// Get source and dest params
var splitAt = _.indexOf( params, 'to' );
if( splitAt === a._.length - 1 ) {
// 'TO' cannot be the last argument
logMsg('Please '.warn + 'specify an output file'.warn.bold +
' for this operation or '.warn + 'omit the TO keyword'.warn.bold +
'.'.warn );
return;
}
var src = a._.slice(1, splitAt === -1 ? undefined : splitAt );
var dst = splitAt === -1 ? [] : a._.slice( splitAt + 1 );
var parms = [ src, dst, opts, logMsg ];
// Invoke the action
FCMD.verbs[ verb ].apply( null, parms );
}
function logMsg( msg ) {
opts.silent || console.log( msg );
}
function getOpts( args ) {
var noPretty = args.nopretty || args.n;
noPretty = noPretty && (noPretty === true || noPretty === 'true');
return {
theme: args.t || 'modern',
format: args.f || 'FRESH',
prettify: !noPretty,
silent: args.s || args.silent
src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
};
}
function handleError( ex ) {
var msg = '', exitCode;
if( ex.fluenterror ){
switch( ex.fluenterror ) { // TODO: Remove magic numbers
case 1: msg = "The specified theme couldn't be found: " + ex.data; break;
case 2: msg = "Couldn't copy CSS file to destination folder"; break;
case 3: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in FRESH or JSON Resume format.'.guide; break;
case 4: msg = title + "\nPlease ".guide + "specify a command".guide.bold + " (".guide +
Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? 'or '.guide : '') +
v.toUpperCase().guide;
}).join(', '.guide) + ").\n\n".guide + FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' ).info.bold;
break;
//case 4: msg = title + '\n' + ; break;
case 5: msg = 'Please '.guide + 'specify the output resume file'.guide.bold + ' that should be created in the new format.'.guide; break;
case 6: msg = 'Please '.guide + 'specify a valid input resume'.guide.bold + ' in either FRESH or JSON Resume format.'.guide; break;
case 7: msg = 'Please '.guide + 'specify an output file name'.guide.bold + ' for every input file you wish to convert.'.guide; break;
}
exitCode = ex.fluenterror;
}
else {
msg = ex.toString();
exitCode = 4;
}
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
if( !ex.fluenterror || ex.fluenterror < 3 )
console.log( ('ERROR: ' + trimmed.toString()).red.bold );
else
console.log( trimmed.toString() );
process.exit( exitCode );
/**
Simple logging placeholder.
*/
function logMsg( msg ) {
msg = msg || '';
_opts.silent || console.log( msg );
}

View File

@ -0,0 +1,151 @@
/**
Employment gap analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module gap-inspector.js
*/
(function() {
var _ = require('underscore');
var FluentDate = require('../core/fluent-date');
var moment = require('moment');
var LO = require('lodash');
/**
Identify gaps in the candidate's employment history.
@class gapInspector
*/
var gapInspector = module.exports = {
moniker: 'gap-inspector',
/**
Run the Gap Analyzer on a resume.
@method run
@return 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 ) {
// This is what we'll return
var coverage = {
gaps: [],
overlaps: [],
duration: {
total: 0,
work: 0,
gaps: 0
},
pct: '0%'
};
// Missing employment section? Bye bye.
var hist = LO.get( rez, 'employment.history' );
if( !hist || !hist.length ) { return coverage; }
// 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.
var new_e = hist.map( function( job ){
var 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;
});
// Flatten the array, remove empties, and sort
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(); });
// Iterate over elements in the array. Each time a start date is found,
// increment a reference count. Each time an end date is found, decrement
// the reference count. When the reference count reaches 0, we have a gap.
// When the reference count is > 0, the candidate is employed. When the
// reference count reaches 2, the candidate is overlapped.
var num_gaps = 0, ref_count = 0, total_gap_days = 0, total_work_days = 0
, gap_start;
new_e.forEach( function(point) {
var inc = point[0] === 'start' ? 1 : -1;
ref_count += inc;
if( ref_count === 0 ) {
coverage.gaps.push( { start: point[1], end: null });
}
else if( ref_count === 1 && inc === 1 ) {
var lastGap = _.last( coverage.gaps );
if( lastGap ) {
lastGap.end = point[1];
lastGap.duration = lastGap.end.diff( lastGap.start, 'days' );
total_gap_days += lastGap.duration;
}
}
else if( ref_count === 2 && inc === 1 ) {
coverage.overlaps.push( { start: point[1], end: null });
}
else if( ref_count === 1 && inc === -1 ) {
var lastOver = _.last( coverage.overlaps );
if( lastOver ) {
lastOver.end = point[1];
lastOver.duration = lastOver.end.diff( lastOver.start, 'days' );
if( lastOver.duration === 0 ) {
coverage.overlaps.pop();
}
total_work_days += lastOver.duration;
}
}
});
// It's possible that the last overlap didn't have an explicit .end date.
// If so, set the end date to the present date and compute the overlap
// duration normally.
if( coverage.overlaps.length ) {
if( !_.last( coverage.overlaps ).end ) {
var l = _.last( coverage.overlaps );
l.end = moment();
l.duration = l.end.diff( l.start, 'days' );
}
}
var dur = {
total: rez.duration('days'),
work: total_work_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;
}
};
}());

View File

@ -0,0 +1,71 @@
/**
Keyword analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module keyword-inspector.js
*/
(function() {
var _ = require('underscore');
var FluentDate = require('../core/fluent-date');
/**
Analyze the resume's use of keywords.
@class keywordInspector
*/
var 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 ) {
// http://stackoverflow.com/a/2593661/4942583
function regex_quote(str) {
return (str + '').replace(/[.?*+^$[\]\\(){}|-]/ig, "\\$&");
}
var searchable = '';
rez.transformStrings( ['imp', 'computed', 'safe'], function trxString( key, val ) {
searchable += ' ' + val;
});
return rez.keywords().map(function(kw) {
//var regex = new RegExp( '\\b' + regex_quote( kw )/* + '\\b'*/, 'ig');
var regex = new RegExp( regex_quote( kw ), 'ig');
var myArray, count = 0;
while ((myArray = regex.exec( searchable )) !== null) {
count++;
}
return {
name: kw,
count: count
};
});
}
};
}());

View File

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

View File

@ -1,25 +1,27 @@
Usage:
fluentcv <COMMAND> <SOURCES> [TO <TARGETS>] [-t <THEME>] [-f <FORMAT>]
hackmyresume <COMMAND> <SOURCES> [TO <TARGETS>] [-t <THEME>] [-f <FORMAT>]
<COMMAND> should be BUILD, NEW, CONVERT, VALIDATE, or HELP. <SOURCES> should
be the path to one or more FRESH or JSON Resume format resumes. <TARGETS>
should be the name of the destination resume to be created, if any. The
<THEME> parameter should be the name of a predefined theme (for example:
COMPACT, MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a custom
theme. <FORMAT> should be either FRESH (for a FRESH-format resume) or JRS
(for a JSON Resume-format resume).
<COMMAND> should be BUILD, NEW, CONVERT, VALIDATE, ANALYZE or HELP. <SOURCES>
should be the path to one or more FRESH or JSON Resume format resumes. <TARGETS>
should be the name of the destination resume to be created, if any. The <THEME>
parameter should be the name of a predefined theme (for example: COMPACT,
MINIMIST, MODERN, or HELLO-WORLD) or the relative path to a custom theme.
<FORMAT> should be either FRESH (for a FRESH-format resume) or JRS (for a JSON
Resume-format resume).
fluentcv BUILD resume.json TO out/resume.all
fluentcv NEW resume.json
fluentcv CONVERT resume.json TO resume-jrs.json
fluentcv VALIDATE resume.json
hackmyresume BUILD resume.json TO out/resume.all
hackmyresume NEW resume.json
hackmyresume CONVERT resume.json TO resume-jrs.json
hackmyresume ANALYZE resume.json
hackmyresume VALIDATE resume.json
Both SOURCES and TARGETS can accept multiple files:
fluentCV BUILD r1.json r2.json TO out/resume.all out2/resume.html
fluentCV NEW r1.json r2.json r3.json
fluentCV VALIDATE resume.json resume2.json resume3.json
hackmyresume BUILD r1.json r2.json TO out/resume.all out2/resume.html
hackmyresume NEW r1.json r2.json r3.json
hackmyresume ANALYZE r1.json r2.json r3.json
hackmyresume VALIDATE resume.json resume2.json resume3.json
See https://github.com/fluentdesk/fluentCV/blob/master/README.md for more
See https://github.com/hacksalot/hackmyresume/blob/master/README.md for more
information.

View File

@ -1,3 +1,8 @@
/**
Definition of John Resig's `Class` class.
@module class.js
*/
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.

View File

@ -1,6 +1,7 @@
/**
Plain JavaScript replacement of jQuery .extend based on jQuery sources.
@license Copyright (c) 2015 by James M. Devlin. All rights reserved.
Definition of the `extend` method.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module extend.js
*/
function _extend() {

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;
};
}());

View File

@ -1,18 +0,0 @@
/**
File-exists checker for Node.js.
@license Copyright (c) 2015 | James M. Devlin
*/
var FS = require('fs');
// Yup, this is now the recommended way to check for file existence on Node.
// fs.exists is deprecated and the recommended fs.statSync/lstatSync throws
// exceptions on non-existent paths :)
module.exports = function (path) {
try {
FS.statSync( path );
return true;
} catch( err ) {
return !(err && err.code === 'ENOENT');
}
};

View File

@ -1,3 +1,8 @@
/**
Definition of the Markdown to WordProcessingML conversion routine.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module html-to-wpml.js
*/
(function(){
@ -6,49 +11,51 @@
module.exports = function( html ) {
var final = '';
var is_bold = false, is_italic = false;
var depth = 0;
// Tokenize the HTML stream.
var tokens = HTML5Tokenizer.tokenize( html );
var final = '', is_bold, is_italic, is_link, link_url;
// Process <em>, <strong>, and <a> elements in the HTML stream, producing
// equivalent WordProcessingML that can be dumped into a <w:p> or other
// text container element.
_.each( tokens, function( tok ) {
switch( tok.type ) {
case 'StartTag':
switch( tok.tagName ) {
case 'p':
final += '<w:p>';
break;
case 'strong':
is_bold = true;
break;
case 'em':
is_italic = true;
break;
case 'p': final += '<w:p>'; break;
case 'strong': is_bold = true; break;
case 'em': is_italic = true; break;
case 'a':
is_link = true;
link_url = tok.attributes.filter(function(attr){
return attr[0] === 'href'; }
)[0][1];
break;
}
break;
case 'EndTag':
switch( tok.tagName ) {
case 'p':
final += '</w:p>';
break;
case 'strong':
is_bold = false;
break;
case 'em':
is_italic = false;
break;
case 'a':
is_link = false;
break;
case 'p': final += '</w:p>'; break;
case 'strong': is_bold = false; break;
case 'em': is_italic = false; break;
case 'a': is_link = false; break;
}
break;
case 'Chars':
if( tok.chars.trim().length ) {
var style = is_bold ? '<w:b/>' : '';
style += is_italic ? '<w:i/>': '';
final += '<w:r><w:rPr>' + style + '</w:rPr><w:t>' + tok.chars + '</w:t></w:r>';
style += is_link ? '<w:rStyle w:val="Hyperlink"/>' : '';
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>' : '');
}
break;
}
});

View File

@ -0,0 +1,24 @@
/**
Definition of the SafeJsonLoader class.
@module syntax-error-ex.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var FS = require('fs');
module.exports = function loadSafeJson( file ) {
try {
return JSON.parse( FS.readFileSync( file ) );
}
catch(ex) {
loadSafeJson.error = ex;
}
return null;
};
}());

View File

@ -1,6 +1,6 @@
/**
String utility functions.
@license Copyright (c) 2015 | James M. Devlin
Definitions of string utility functions.
@module string.js
*/
/**
@ -9,10 +9,18 @@ See: http://stackoverflow.com/a/32800728/4942583
@method isNullOrWhitespace
*/
String.isNullOrWhitespace = function( input ) {
return !input || !input.trim();
};
(function() {
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
String.isNullOrWhitespace = function( input ) {
return !input || !input.trim();
};
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
String.is = function( val ) {
return typeof val === 'string' || val instanceof String;
};
}());

View File

@ -0,0 +1,41 @@
/**
Definition of the SyntaxErrorEx class.
@module syntax-error-ex.js
@license MIT. See LICENSE.md for details.
*/
(function() {
/**
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 first check for a .lineNumber and .columnNumber and, if that's
not present, fall back to the JSONLint library, which provides that info.
See: http://stackoverflow.com/q/13323356
@class SyntaxErrorEx
*/
module.exports = function SyntaxErrorEx( ex, rawData ) {
var lineNum = null, colNum = null;
if( ex.lineNumber !== undefined && ex.lineNumber !== null ) {
lineNum = ex.lineNumber;
}
if( ex.columnNumber !== undefined && ex.columnNumber !== null ) {
colNum = ex.columnNumber;
}
if( lineNum === null || colNum === null ) {
var JSONLint = require('json-lint'); // TODO: json-lint or is-my-json-valid?
var lint = JSONLint( rawData, { comments: false } );
if( lineNum === null ) lineNum = (lint.error ? lint.line : '???');
if( colNum === null ) colNum = (lint.error ? lint.character : '???');
}
this.line = lineNum;
this.col = colNum;
};
}());

107
src/verbs/analyze.js Normal file
View File

@ -0,0 +1,107 @@
/**
Implementation of the 'analyze' verb for HackMyResume.
@module create.js
@license MIT. See LICENSE.md for details.
*/
(function(){
var MKDIRP = require('mkdirp')
, PATH = require('path')
, _ = require('underscore')
, ResumeFactory = require('../core/resume-factory')
, chalk = require('chalk');
/**
Run the 'analyze' command.
*/
module.exports = function analyze( sources, dst, opts, logger ) {
var _log = logger || console.log;
if( !sources || !sources.length ) throw { fluenterror: 3 };
var nlzrs = _loadInspectors();
sources.forEach( function(src) {
var result = ResumeFactory.loadOne( src, {
log: _log, format: 'FRESH', objectify: true, throw: false
});
result.error || _analyze( result, nlzrs, opts, _log );
});
};
/**
Analyze a single resume.
*/
function _analyze( resumeObject, nlzrs, opts, log ) {
var rez = resumeObject.rez;
var safeFormat =
(rez.meta && rez.meta.format && rez.meta.format.startsWith('FRESH')) ?
'FRESH' : 'JRS';
var padding = 20;
log(chalk.cyan('Analyzing ') + chalk.cyan.bold(safeFormat) +
chalk.cyan(' resume: ') + chalk.cyan.bold(resumeObject.file));
var info = _.mapObject( nlzrs, function(val, key) {
return val.run( resumeObject.rez );
});
log(chalk.cyan.bold('\nSECTIONS') + chalk.cyan(' (') + chalk.white.bold(_.keys(info.totals).length) + chalk.cyan('):\n'));
var pad = require('string-padding');
_.each( info.totals, function(tot, key) {
log(chalk.cyan(pad(key + ': ',20)) + chalk.cyan.bold(pad(tot.toString(),5)));
});
log();
log(chalk.cyan.bold('COVERAGE') + chalk.cyan(' (') + chalk.white.bold( info.coverage.pct ) + chalk.cyan('):\n'));
log(chalk.cyan(pad('Total Days: ', padding)) + chalk.cyan.bold( pad(info.coverage.duration.total.toString(),5) ));
log(chalk.cyan(pad('Employed: ', padding)) + chalk.cyan.bold( pad((info.coverage.duration.total - info.coverage.duration.gaps).toString(),5) ));
log(chalk.cyan(pad('Gaps: ', padding + 4)) + chalk.cyan.bold(info.coverage.gaps.length) + chalk.cyan(' [') + info.coverage.gaps.map(function(g) {
var clr = 'green';
if( g.duration > 35 ) clr = 'yellow';
if( g.duration > 90 ) clr = 'red';
return chalk[clr].bold( g.duration) ;
}).join(', ') + chalk.cyan(']') );
log(chalk.cyan(pad('Overlaps: ', padding + 4)) + chalk.cyan.bold(info.coverage.overlaps.length) + chalk.cyan(' [') + info.coverage.overlaps.map(function(ol) {
var clr = 'green';
if( ol.duration > 35 ) clr = 'yellow';
if( ol.duration > 90 ) clr = 'red';
return chalk[clr].bold( ol.duration) ;
}).join(', ') + chalk.cyan(']') );
var tot = 0;
log();
log( chalk.cyan.bold('KEYWORDS') + chalk.cyan(' (') + chalk.white.bold( info.keywords.length ) +
chalk.cyan('):\n\n') +
info.keywords.map(function(g) {
tot += g.count;
return chalk.cyan( pad(g.name + ': ', padding) ) + chalk.cyan.bold( pad( g.count.toString(), 5 )) + chalk.cyan(' mentions');
}).join('\n'));
log(chalk.cyan( pad('TOTAL: ', padding) ) + chalk.white.bold( pad( tot.toString(), 5 )) + chalk.cyan(' mentions'));
}
/**
Load inspectors.
*/
function _loadInspectors() {
return {
totals: require('../inspectors/totals-inspector'),
coverage: require('../inspectors/gap-inspector'),
keywords: require('../inspectors/keyword-inspector')
};
}
}());

69
src/verbs/convert.js Normal file
View File

@ -0,0 +1,69 @@
/**
Implementation of the 'convert' verb for HackMyResume.
@module convert.js
@license MIT. See LICENSE.md for details.
*/
(function(){
var ResumeFactory = require('../core/resume-factory')
, chalk = require('chalk')
, HACKMYSTATUS = require('../core/status-codes');
/**
Convert between FRESH and JRS formats.
*/
module.exports = function convert( srcs, dst, opts, logger ) {
// Housekeeping
var _log = logger || console.log;
if( !srcs || !srcs.length ) { throw { fluenterror: 6 }; }
if( !dst || !dst.length ) {
if( srcs.length === 1 ) {
throw { fluenterror: HACKMYSTATUS.inputOutputParity };
}
else if( srcs.length === 2 ) {
dst = dst || []; dst.push( srcs.pop() );
}
else {
throw { fluenterror: HACKMYSTATUS.inputOutputParity };
}
}
if(srcs && dst && srcs.length && dst.length && srcs.length !== dst.length){
throw { fluenterror: HACKMYSTATUS.inputOutputParity };
}
// Load source resumes
srcs.forEach( function( src, idx ) {
// Load the resume
var rinfo = ResumeFactory.loadOne( src, {
log: _log, format: null, objectify: true, throw: true
});
var s = rinfo.rez
, srcFmt = ((s.basics && s.basics.imp) || s.imp).orgFormat === 'JRS' ?
'JRS' : 'FRESH'
, targetFormat = srcFmt === 'JRS' ? 'FRESH' : 'JRS';
// TODO: Core should not log
_log( chalk.green('Converting ') + chalk.green.bold(rinfo.file) +
chalk.green(' (' + srcFmt + ') to ') + chalk.green.bold(dst[idx]) +
chalk.green(' (' + targetFormat + ').'));
// Save it to the destination format
s.saveAs( dst[idx], targetFormat );
});
};
}());

31
src/verbs/create.js Normal file
View File

@ -0,0 +1,31 @@
/**
Implementation of the 'create' verb for HackMyResume.
@module create.js
@license MIT. See LICENSE.md for details.
*/
(function(){
var MKDIRP = require('mkdirp')
, PATH = require('path')
, chalk = require('chalk')
, HACKMYSTATUS = require('../core/status-codes');
/**
Create a new empty resume in either FRESH or JRS format.
*/
module.exports = function create( src, dst, opts, logger ) {
var _log = logger || console.log;
if( !src || !src.length ) throw { fluenterror: HACKMYSTATUS.createNameMissing };
src.forEach( function( t ) {
var safeFormat = opts.format.toUpperCase();
_log(chalk.green('Creating new ') + chalk.green.bold(safeFormat) +
chalk.green(' resume: ') + chalk.green.bold(t));
MKDIRP.sync( PATH.dirname( t ) ); // Ensure dest folder exists;
var RezClass = require('../core/' + safeFormat.toLowerCase() + '-resume' );
RezClass.default().save(t);
//FLUENT[ safeFormat + 'Resume' ].default().save( t );
});
};
}());

346
src/verbs/generate.js Normal file
View File

@ -0,0 +1,346 @@
/**
Implementation of the 'generate' verb for HackMyResume.
@module generate.js
@license MIT. See LICENSE.md for details.
*/
// TODO: EventEmitter
(function() {
var PATH = require('path')
, FS = require('fs')
, MD = require('marked')
, MKDIRP = require('mkdirp')
, EXTEND = require('../utils/extend')
, parsePath = require('parse-filepath')
, _opts = require('../core/default-options')
, FluentTheme = require('../core/fresh-theme')
, JRSTheme = require('../core/jrs-theme')
, ResumeFactory = require('../core/resume-factory')
, _ = require('underscore')
, _fmts = require('../core/default-formats')
, extend = require('../utils/extend')
, chalk = require('chalk')
, pad = require('string-padding')
, _err, _log, rez;
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
/**
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 theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
*/
function build( src, dst, opts, logger, errHandler ) {
// Housekeeping
//_opts = extend( true, _opts, opts );
_log = logger || console.log;
_err = errHandler || error;
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
_opts.css = opts.css || 'embed';
_opts.pdf = opts.pdf;
_opts.wrap = opts.wrap || 60;
_opts.stitles = opts.sectionTitles;
_opts.tips = opts.tips;
//_opts.noTips = opts.noTips;
// If two or more files are passed to the GENERATE command and the TO
// keyword is omitted, the last file specifies the output file.
if( src.length > 1 && ( !dst || !dst.length ) ) {
dst.push( src.pop() );
}
// Load the theme...we do this first because the theme choice (FRESH or
// JSON Resume) determines what format we'll convert the resume to.
var tFolder = verify_theme( _opts.theme );
var theme = load_theme( tFolder );
// Load input resumes...
if( !src || !src.length ) { throw { fluenterror: 3 }; }
var sheets = ResumeFactory.load(src, {
log: _log, format: theme.render ? 'JRS' : 'FRESH',
objectify: true, throw: true
}).map(function(sh){ return sh.rez; });
// Merge input resumes...
var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ?
chalk.cyan('Merging ') + chalk.cyan.bold(a.i().file) : '') +
chalk.cyan(' onto ') + chalk.cyan.bold(b.i().file);
return extend( true, b, a );
});
msg && _log(msg);
// Output theme messages
var numFormats = Object.keys(theme.formats).length;
var themeName = theme.name.toUpperCase();
_log( chalk.yellow('Applying ') + chalk.yellow.bold(themeName) +
chalk.yellow(' theme (' + numFormats + ' format' +
( numFormats === 1 ? ')' : 's)') ));
// Expand output resumes...
var targets = expand( dst, theme );
// Run the transformation!
targets.forEach( function(t) {
t.final = single( t, theme, targets );
});
if( _opts.tips && (theme.message || theme.render) ) {
var WRAP = require('word-wrap');
if( theme.message ) {
_log( WRAP( chalk.gray('The ' + themeName +
' theme says: "') + chalk.white(theme.message) + chalk.gray('"'),
{ width: _opts.wrap, indent: '' } ));
}
else {
_log( WRAP( chalk.gray('The ' + themeName +
' theme says: "') + chalk.white('For best results view JSON Resume ' +
'themes over a local or remote HTTP connection. For example:'),
{ width: _opts.wrap, indent: '' }
));
_log('');
_log(
' npm install http-server -g\r' +
' http-server <resume-folder>' );
_log('');
_log(chalk.white('For more information, see the README."'),
{ width: _opts.wrap, indent: '' } );
}
}
// Don't send the client back empty-handed
return { sheet: rez, targets: targets, processed: targets };
}
/**
Generate a single target resume such as "out/rez.html" or "out/rez.doc".
@param targInfo Information for the target resume.
@param theme A FRESHTheme or JRSTheme object.
@returns
*/
function single( targInfo, theme, finished ) {
function MDIN(txt) { // TODO: Move this
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
try {
var f = targInfo.file
, fType = targInfo.fmt.outFormat
, fName = PATH.basename(f, '.' + fType)
, theFormat;
var suffix = '';
if( targInfo.fmt.outFormat === 'pdf' ) {
if( _opts.pdf ) {
if( _opts.pdf !== 'none' ) {
suffix = chalk.green(' (with ' + _opts.pdf + ')');
}
else {
_log( chalk.gray('Skipping ') +
chalk.white.bold(
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
chalk.gray(' resume') + suffix + chalk.green(': ') +
chalk.white( PATH.relative(process.cwd(), f )) );
return;
}
}
}
_log( chalk.green('Generating ') +
chalk.green.bold(
pad(targInfo.fmt.outFormat.toUpperCase(),4,null,pad.RIGHT)) +
chalk.green(' resume') + suffix + chalk.green(': ') +
chalk.green.bold( PATH.relative(process.cwd(), f )) );
// If targInfo.fmt.files exists, this format is backed by a document.
// Fluent/FRESH themes are handled here.
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 ) ); // Ensure dest folder exists;
_opts.targets = finished;
return theFormat.gen.generate( rez, f, _opts );
}
// Otherwise this is either a) a JSON Resume theme or b) an ad-hoc format
// (JSON, YML, or PNG) that every theme gets "for free".
else {
theFormat = _fmts.filter( function(fmt) {
return fmt.name === targInfo.fmt.outFormat;
})[0];
var outFolder = PATH.dirname( f );
MKDIRP.sync( outFolder ); // Ensure dest folder exists;
// JSON Resume themes have a 'render' method that needs to be called
if( theme.render ) {
var COPY = require('copy');
var globs = [ '*.css', '*.js', '*.png', '*.jpg', '*.gif', '*.bmp' ];
COPY.sync( globs , outFolder, {
cwd: theme.folder, nodir: true,
ignore: ['node_modules/','node_modules/**']
// rewrite: function(p1, p2) {
// return PATH.join(p2, p1);
// }
});
// Prevent JSON Resume theme .js from chattering (TODO: redirect IO)
var consoleLog = console.log;
console.log = function() { };
// Call the theme's render method
var rezDupe = rez.harden();
var rezHtml = theme.render( rezDupe );
// Turn logging back on
console.log = consoleLog;
// Unharden
rezHtml = rezHtml.replace( /@@@@~.*?~@@@@/gm, function(val){
return MDIN( val.replace( /~@@@@/gm,'' ).replace( /@@@@~/gm,'' ) );
});
// Save the file
FS.writeFileSync( f, rezHtml );
// Return markup to the client
return rezHtml;
}
else {
return theFormat.gen.generate( rez, f, _opts );
}
}
}
catch( ex ) {
_err( ex );
}
}
/**
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.
*/
function expand( dst, theTheme ) {
// Add freebie formats (JSON, YAML, PNG) every theme gets...
// Add HTML-driven PNG only if the theme has an HTML format.
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
};
}
// Set up the destination collection. It's either the array of files passed
// by the user or 'out/resume.all' if no targets were specified.
var destColl = (dst && dst.length && dst) ||
[PATH.normalize('out/resume.all')];
// Assemble an array of expanded target files... (can't use map() here)
var targets = [];
destColl.forEach( function(t) {
var to = PATH.resolve(t), pa = parsePath(to),fmat = pa.extname || '.all';
targets.push.apply(
targets, fmat === '.all' ?
Object.keys( theTheme.formats ).map( function( k ) {
var z = theTheme.formats[k];
return { file: to.replace( /all$/g, z.outFormat ), fmt: z };
}) :
[{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
// targets.push.apply(
// targets, fmat === '.all' ?
// Object.keys( explicitFormats ).map( function( k ) {
// var z = theTheme.formats[k];
// return { file: to.replace( /all$/g, z.outFormat ), fmt: z };
// }) :
// [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
});
return targets;
}
/**
Verify the specified theme name/path.
*/
function verify_theme( themeNameOrPath ) {
var tFolder = PATH.join(
parsePath ( require.resolve('fresh-themes') ).dirname,
'/themes/',
themeNameOrPath
);
var exists = require('path-exists').sync;
if( !exists( tFolder ) ) {
tFolder = PATH.resolve( themeNameOrPath );
if( !exists( tFolder ) ) {
throw { fluenterror: 1, data: _opts.theme };
}
}
return tFolder;
}
/**
Load the specified theme.
*/
function load_theme( tFolder ) {
// Create a FRESH or JRS theme object
var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ?
new JRSTheme().open(tFolder) : new FluentTheme().open( tFolder );
// Cache the theme object
_opts.themeObj = theTheme;
return theTheme;
}
module.exports = build;
}());

104
src/verbs/validate.js Normal file
View File

@ -0,0 +1,104 @@
/**
Implementation of the 'validate' verb for HackMyResume.
@module validate.js
@license MIT. See LICENSE.md for details.
*/
(function() {
var FS = require('fs');
var ResumeFactory = require('../core/resume-factory');
var SyntaxErrorEx = require('../utils/syntax-error-ex');
var chalk = require('chalk');
var HACKMYSTATUS = require('../core/status-codes');
module.exports =
/**
Validate 1 to N resumes in either FRESH or JSON Resume format.
*/
function validate( sources, unused, opts, logger ) {
var _log = logger || console.log;
if( !sources || !sources.length ) { throw { fluenterror: 6 }; }
var isValid = true;
var validator = require('is-my-json-valid');
var schemas = {
fresh: require('fresca'),
jars: require('../core/resume.json')
};
var resumes = ResumeFactory.load( sources, {
log: _log,
format: null,
objectify: false,
throw: false,
muffle: true
});
// Load input resumes...
resumes.forEach(function( src ) {
if( src.error ) {
// TODO: Core should not log
_log( chalk.white('Validating ') + chalk.gray.bold(src.file) +
chalk.white(' against ') + chalk.gray.bold('AUTO') +
chalk.white(' schema:') + chalk.red.bold(' BROKEN') );
var ex = src.error; // alias
if ( ex instanceof SyntaxError) {
var info = new SyntaxErrorEx( ex, src.raw );
_log( chalk.red.bold('--> ' + src.file.toUpperCase() + ' contains invalid JSON on line ' +
info.line + ' column ' + info.col + '.' +
chalk.red(' Unable to validate.') ) );
_log( chalk.red.bold(' INTERNAL: ' + ex) );
}
else {
_log(chalk.red.bold('ERROR: ' + ex.toString()));
}
if( opts.assert ) throw { fluenterror: HACKMYSTATUS.invalid };
return;
}
var json = src.json;
var isValid = false;
var style = 'green';
var errors = [];
var fmt = json.basics ? 'jrs' : 'fresh';
try {
var validate = validator( schemas[ fmt ], { // Note [1]
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
isValid = validate( json );
if( !isValid ) {
style = 'yellow';
errors = validate.errors;
}
}
catch(exc) {
return;
}
_log( chalk.white('Validating ') + chalk.white.bold(src.file) + chalk.white(' against ') +
chalk.white.bold(fmt.replace('jars','JSON Resume').toUpperCase()) +
chalk.white(' schema: ') + chalk[style].bold(isValid ? 'VALID!' : 'INVALID') );
errors.forEach(function(err,idx) {
_log( chalk.yellow.bold('--> ') +
chalk.yellow(err.field.replace('data.','resume.').toUpperCase() + ' ' +
err.message) );
});
if( opts.assert && !isValid ) {
throw { fluenterror: HACKMYSTATUS.invalid, shouldExit: true };
}
});
};
}());

1
test/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
sandbox/

View File

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

View File

@ -0,0 +1,104 @@
{
"basics": {
"name": "Jane Q. Fullstacker"
},
"education": [
{
"gpa": "3.5",
"courses": [
"Course 1",
"Course 2",
"Course 2"
],
"startDate": "2005-09",
"endDate": "2008-05"
},
{
"institution": "Medfield College",
"gpa": "3.2",
"courses": [
"Course 1",
"Course 2",
"Course 2"
],
"startDate": "2003-09",
"endDate": "2005-06"
}
],
"skills": [
{
"name": "Web Dev",
"keywords": [
"JavaScript",
"HTML 5",
"CSS",
"LAMP",
"MVC",
"REST"
]
},
{
"name": "JavaScript"
}
],
"volunteer": [],
"publications": [
{
"name": "Building User Interfaces with Electron and Atom",
"releaseDate": "2011",
"website": "http://codeproject.com/build-ui-electron-atom.aspx"
},
{
"name": "Jane Fullstacker's Blog",
"publisher": "self",
"releaseDate": "2011",
"website": "http://janef.me"
},
{
"name": "Teach Yourself GORFF in 21 Days",
"publisher": "Amazon"
}
],
"interests": [
{
"name": "reading",
"summary": "Jane is a fan of mystery novels and courtroom dramas including Agatha Christie and John Grisham.",
"keywords": [
"mystery",
"Agatha Christie",
"John Grisham"
]
},
{
"name": "hiking",
"summary": "Jane enjoys hiking, light mountain climbing, and has four summits under her belt!"
},
{
"name": "yoga"
}
],
"references": [
{
"name": "John Davidson",
"reference": "Jane is awesome! I'd hire her again in a heartbeat."
},
{
"name": "Elias Fullstacker",
"reference": "I worked with Jane on Jabberwocky and can vouch for her awesome technical capabilities and attention to detail. Insta-hire."
},
{
"name": "Dana Nevins",
"reference": "I've known Jane personally and professionally for almost ten years. She is one in a million."
}
],
"languages": [
{
"language": "English",
"level": "Native"
},
{
"language": "Spanish",
"level": "Moderate"
}
]
}

View File

@ -1,14 +1,14 @@
{
"basics": {
"name": "Jane Doe",
"label": "Senior Developer / Code Ninja",
"name": "Jane Q. Fullstacker",
"label": "Senior Developer",
"summary": "**Full-stack software developer with 6+ years industry experience** specializing in scalable cloud architectures for this, that, and the other. A native of southern CA, Jane enjoys hiking, mystery novels, and the company of Rufus, her two-year-old beagle.",
"website": "http://jane-doe.me",
"website": "http://janef.me/blog",
"phone": "1-650-999-7777",
"email": "jdoe@onecoolstartup.io",
"picture": "jane_doe.png",
"location": {
"address": "Jane Doe\n123 Somewhere Rd.\nMountain View, CA 94035",
"address": "Jane Fullstacker\n123 Somewhere Rd.\nMountain View, CA 94035",
"postalCode": "94035",
"city": "Mountain View",
"countryCode": "US",
@ -17,13 +17,13 @@
"profiles": [
{
"network": "GitHub",
"username": "jane-doe-was-here",
"url": "https://github.com/jane-doe-was-here"
"username": "janef-was-here",
"url": "https://github.com/janef-was-here"
},
{
"network": "Twitter",
"username": "jane-doe-was-here",
"url": "https://twitter.com/jane-doe-was-here"
"username": "janef-was-here",
"url": "https://twitter.com/janef-was-here"
}
]
},
@ -104,17 +104,55 @@
],
"skills": [
{
"name": "Programming",
"name": "Web Dev",
"keywords": [
"C++",
"Ruby",
"Xcode"
"JavaScript",
"HTML 5",
"CSS",
"LAMP",
"MVC",
"REST"
]
},
{
"name": "Project Management",
"name": "JavaScript",
"keywords": [
"Agile"
"Node.js",
"Angular.js",
"jQuery",
"Bootstrap",
"React.js",
"Backbone.js"
]
},
{
"name": "Database",
"keywords": [
"MySQL",
"PostgreSQL",
"NoSQL",
"ORM",
"Hibernate"
]
},
{
"name": "Cloud",
"keywords": [
"AWS",
"EC2",
"RDS",
"S3",
"Azure",
"Dropbox"
]
},
{
"name": "Project",
"keywords": [
"Agile",
"TFS",
"Unified Process",
"MS Project"
]
}
],
@ -166,10 +204,10 @@
"website": "http://codeproject.com/build-ui-electron-atom.aspx"
},
{
"name": "Jane Doe Unplugged",
"name": "Jane Fullstacker's Blog",
"publisher": "self",
"releaseDate": "2011",
"website": "http://jane-doe.me"
"website": "http://janef.me"
},
{
"name": "Teach Yourself GORFF in 21 Days",
@ -218,7 +256,8 @@
},
{
"language": "Spanish",
"level": "Moderate"
"level": "Moderate",
"years": 10
}
]
}
}

76
test/test-cli.js Normal file
View File

@ -0,0 +1,76 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, FCMD = require( '../src/hackmyapi')
, validator = require('is-my-json-valid');
chai.config.includeStack = false;
describe('Testing CLI interface', function () {
var _sheet;
function logMsg() {
}
var opts = {
format: 'FRESH',
prettify: true,
silent: false,
assert: true // Causes validation errors to throw exceptions
};
var opts2 = {
format: 'JRS',
prettify: true,
silent: true
};
run( 'new', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (FRESH format)' );
run( 'new', ['test/sandbox/new-jrs-resume.json'], [], opts2, ' (JRS format)' );
run( 'new', ['test/sandbox/new-1.json', 'test/sandbox/new-2.json', 'test/sandbox/new-3.json'], [], opts, ' (multiple FRESH resumes)' );
run( 'new', ['test/sandbox/new-jrs-1.json', 'test/sandbox/new-jrs-2.json', 'test/sandbox/new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' );
fail( 'new', [], [], opts, " (when a filename isn't specified)" );
run( 'validate', ['node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json'], [], opts, ' (jane-q-fullstacker|FRESH)' );
run( 'validate', ['node_modules/fresh-test-resumes/src/johnny-trouble.fresh.json'], [], opts, ' (johnny-trouble|FRESH)' );
run( 'validate', ['test/sandbox/new-fresh-resume.json'], [], opts, ' (new-fresh-resume|FRESH)' );
run( 'validate', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks.json|JRS)' );
run( 'validate', ['test/resumes/jrs-0.0.0/jane-incomplete.json'], [], opts2, ' (jane-incomplete.json|JRS)' );
run( 'validate', ['test/sandbox/new-1.json','test/sandbox/new-jrs-resume.json','test/sandbox/new-1.json', 'test/sandbox/new-2.json', 'test/sandbox/new-3.json'], [], opts, ' (5|BOTH)' );
run( 'analyze', ['node_modules/fresh-test-resumes/src/jane-fullstacker.json'], [], opts, ' (jane-q-fullstacker|FRESH)' );
run( 'analyze', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks|JRS)' );
run( 'build',
[ 'node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json',
'node_modules/fresh-test-resumes/src/override/jane-fullstacker-override.fresh.json' ],
[ 'test/sandbox/merged/jane-fullstacker-gamedev.fresh.all'], opts, ' (jane-q-fullstacker w/ override|FRESH)'
);
function run( verb, src, dst, opts, msg ) {
msg = msg || '.';
it( 'The ' + verb.toUpperCase() + ' command should SUCCEED' + msg, function () {
function runIt() {
FCMD.verbs[verb]( src, dst, opts, opts.silent ? logMsg : function(msg){ msg = msg || ''; console.log(msg); } );
}
runIt.should.not.Throw();
});
}
function fail( verb, src, dst, opts, msg ) {
msg = msg || '.';
it( 'The ' + verb.toUpperCase() + ' command should FAIL' + msg, function () {
function runIt() {
FCMD.verbs[verb]( src, dst, opts, logMsg );
}
runIt.should.Throw();
});
}
});

View File

@ -3,10 +3,12 @@ var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, parsePath = require('parse-filepath')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, CONVERTER = require('../src/core/convert')
, FS = require('fs')
, MKDIRP = require('mkdirp')
, _ = require('underscore');
chai.config.includeStack = false;
@ -17,10 +19,11 @@ describe('FRESH/JRS converter', function () {
it('should round-trip from JRS to FRESH to JRS without modifying or losing data', function () {
var fileA = path.join( __dirname, 'resumes/jrs/richard-hendriks.json' );
var fileA = path.join( __dirname, 'resumes/jrs-0.0.0/richard-hendriks.json' );
var fileB = path.join( __dirname, 'sandbox/richard-hendriks.json' );
_sheet = new FRESHResume().open( fileA );
MKDIRP.sync( parsePath( fileB ).dirname );
_sheet.saveAs( fileB, 'JRS' );
var rawA = FS.readFileSync( fileA, 'utf8' );

62
test/test-fresh-sheet.js Normal file
View File

@ -0,0 +1,62 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, validator = require('is-my-json-valid');
chai.config.includeStack = false;
function testResume(opts) {
describe( opts.title + ' (FRESH)', function () {
var _sheet;
it('should open without throwing an exception', function () {
function tryOpen() {
_sheet = new FRESHResume().open( opts.path );
}
tryOpen.should.not.Throw();
});
it('should have one or more of each section', function() {
var newObj = _.pick( _sheet, opts.sections );
expect( Object.keys(newObj).length ).to.equal( opts.sections.length );
});
it('should have a work duration of ' + opts.duration + ' years', function() {
_sheet.computed.numYears.should.equal( opts.duration );
});
it('should save without throwing an exception', function(){
function trySave() {
_sheet.save( 'test/sandbox/' + opts.title + '.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new FRESHResume().open('test/sandbox/' + opts.title + '.json');
_sheet.stringify().should.equal( savedSheet.stringify() );
});
it('should validate against the FRESH resume schema', function() {
var result = _sheet.isValid();
// var schemaJson = require('fresca');
// var validate = validator( schemaJson, { verbose: true } );
// var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n");
result.should.equal( true );
});
});
}
var sects = [ 'info', 'employment', 'service', 'skills', 'education', 'writing', 'recognition', 'references' ];
testResume({ title: 'jane-q-fullstacker', path: 'node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json', duration: 7, sections: sects });
testResume({ title: 'johnny-trouble-resume', path: 'node_modules/fresh-test-resumes/src/johnny-trouble.fresh.json', duration: 4, sections: sects });

69
test/test-jrs-sheet.js Normal file
View File

@ -0,0 +1,69 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, JRSResume = require('../src/core/jrs-resume')
, validator = require('is-my-json-valid');
chai.config.includeStack = false;
function testResume( opts ) {
describe( opts.title + ' (JRS)', function() {
opts.isValid = opts.isValid !== false;
var _sheet;
it('should open without throwing an exception', function () {
var that = this;
function tryOpen() {
_sheet = new JRSResume().open(
path.join( __dirname, 'resumes/jrs-0.0.0/' + opts.title + '.json' ) );
}
tryOpen.should.not.Throw();
});
it('should have one or more of each section', function() {
var newObj = _.pick( _sheet, opts.sections );
expect( Object.keys(newObj).length ).to.equal( opts.sections.length );
});
it('should have a work duration of ' + opts.duration + ' years', function() {
_sheet.basics.computed.numYears.should.equal( opts.duration );
});
it('should save without throwing an exception', function() {
var that = this;
function trySave() {
_sheet.save( 'test/sandbox/' + opts.title + '.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new JRSResume().open( 'test/sandbox/' + opts.title + '.json' );
_sheet.stringify().should.equal( savedSheet.stringify() );
});
it('should ' + (opts.isValid ? '' : 'NOT ') + 'validate against the JSON Resume schema', function() {
var result = _sheet.isValid();
// var schemaJson = require('../src/core/resume.json');
// var validate = validator( schemaJson, { verbose: true } );
// var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", _sheet.basics.imp.validationErrors, "\n\n");
result.should.equal( opts.isValid );
});
});
}
var sects = [ 'basics', 'work', 'volunteer', 'skills', 'education', 'publications', 'awards', 'references' ];
testResume({ title: 'jane-q-fullstacker', duration: 7, sections: sects });
testResume({ title: 'jane-incomplete', duration: 0, sections: _.without(sects, 'awards', 'work') });
testResume({ title: 'richard-hendriks', duration: 1, sections: sects });
testResume({ title: 'empty', duration: 0, sections: [], isValid: false });

87
test/test-themes.js Normal file
View File

@ -0,0 +1,87 @@
var SPAWNWATCHER = require('../src/core/spawn-watch')
, chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, HMR = require( '../src/hackmyapi')
, validator = require('is-my-json-valid')
, READFILES = require('recursive-readdir-sync')
, fileContains = require('../src/utils/file-contains')
, FS = require('fs');
chai.config.includeStack = true;
function genThemes( title, src, fmt ) {
describe('Testing themes against ' + title.toUpperCase() + ' resume ' + '(' + fmt + ')' , function () {
var _sheet;
function genTheme( fmt, src, themeName, themeLoc, testTitle ) {
themeLoc = themeLoc || themeName;
testTitle = themeName.toUpperCase() + ' theme (' + fmt + ') should generate without throwing an exception';
it( testTitle, function () {
function tryOpen() {
//var src = ['node_modules/jane-q-fullstacker/resume/jane-resume.json'];
var dst = ['test/sandbox/' + fmt + '/' + title + '/' + themeName + '/resume.all'];
var opts = {
theme: themeLoc,
format: fmt,
prettify: true,
silent: false,
css: 'embed'
};
try {
HMR.verbs.build( src, dst, opts, function(msg) {
msg = msg || '';
console.log(msg);
});
}
catch(ex) {
console.log(ex);
console.log(ex.stack);
throw ex;
}
}
tryOpen.should.not.Throw();
});
}
genTheme(fmt, src, 'hello-world');
genTheme(fmt, src, 'compact');
genTheme(fmt, src, 'modern');
genTheme(fmt, src, 'minimist');
genTheme(fmt, src, 'awesome');
genTheme(fmt, src, 'positive');
genTheme(fmt, src, 'jsonresume-theme-boilerplate', 'node_modules/jsonresume-theme-boilerplate' );
genTheme(fmt, src, 'jsonresume-theme-sceptile', 'node_modules/jsonresume-theme-sceptile' );
genTheme(fmt, src, 'jsonresume-theme-modern', 'node_modules/jsonresume-theme-modern' );
genTheme(fmt, src, 'jsonresume-theme-classy', 'node_modules/jsonresume-theme-classy' );
});
}
function folderContains( needle, haystack ) {
return _.some( READFILES( path.join(__dirname, haystack) ), function( absPath ) {
if( FS.lstatSync( absPath ).isFile() ) {
if( fileContains( absPath, needle ) ) {
console.log('Found invalid metadata in ' + absPath);
return true;
}
}
});
}
genThemes( 'jane-q-fullstacker', ['node_modules/fresh-test-resumes/src/jane-fullstacker.fresh.json'], 'FRESH' );
genThemes( 'johnny-trouble', ['node_modules/fresh-test-resumes/src/johnny-trouble.fresh.json'], 'FRESH' );
genThemes( 'richard-hendriks', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], 'JRS' );
describe('Verifying generated theme files...', function() {
it('Generated files should not contain ICE.', function() {
expect( folderContains('@@@@', 'sandbox') ).to.be.false;
});
});

View File

@ -1,67 +0,0 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, validator = require('is-my-json-valid');
chai.config.includeStack = false;
describe('jane-doe.json (FRESH)', function () {
var _sheet;
it('should open without throwing an exception', function () {
function tryOpen() {
_sheet = new FRESHResume().open(
'node_modules/FRESCA/exemplar/jane-doe.json' );
}
tryOpen.should.not.Throw();
});
it('should have one or more of each section', function() {
expect(
//(_sheet.basics) &&
_sheet.name && _sheet.info && _sheet.location && _sheet.contact &&
(_sheet.employment.history && _sheet.employment.history.length > 0) &&
(_sheet.skills && _sheet.skills.list.length > 0) &&
(_sheet.education.history && _sheet.education.history.length > 0) &&
(_sheet.service.history && _sheet.service.history.length > 0) &&
(_sheet.writing && _sheet.writing.length > 0) &&
(_sheet.recognition && _sheet.recognition.length > 0) &&
(_sheet.samples && _sheet.samples.length > 0) &&
(_sheet.references && _sheet.references.length > 0) &&
(_sheet.interests && _sheet.interests.length > 0)
).to.equal( true );
});
it('should have a work duration of 7 years', function() {
_sheet.computed.numYears.should.equal( 7 );
});
it('should save without throwing an exception', function(){
function trySave() {
_sheet.save( 'tests/sandbox/jane-doe.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new FRESHResume().open('tests/sandbox/jane-doe.json');
_sheet.stringify().should.equal( savedSheet.stringify() )
});
it('should validate against the FRESH resume schema', function() {
var result = _sheet.isValid();
// var schemaJson = require('FRESCA');
// var validate = validator( schemaJson, { verbose: true } );
// var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", _sheet.imp.validationErrors, "\n\n");
result.should.equal( true );
});
});

View File

@ -1,63 +0,0 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, JRSResume = require('../src/core/jrs-resume')
, validator = require('is-my-json-valid');
chai.config.includeStack = false;
describe('jane-doe.json (JRS)', function () {
var _sheet;
it('should open without throwing an exception', function () {
function tryOpen() {
_sheet = new JRSResume().open(
path.join( __dirname, 'resumes/jrs/jane-doe.json' ) );
}
tryOpen.should.not.Throw();
});
it('should have one or more of each section', function() {
expect(
(_sheet.basics) &&
(_sheet.work && _sheet.work.length > 0) &&
(_sheet.skills && _sheet.skills.length > 0) &&
(_sheet.education && _sheet.education.length > 0) &&
(_sheet.volunteer && _sheet.volunteer.length > 0) &&
(_sheet.publications && _sheet.publications.length > 0) &&
(_sheet.awards && _sheet.awards.length > 0)
).to.equal( true );
});
it('should have a work duration of 7 years', function() {
_sheet.basics.computed.numYears.should.equal( 7 );
});
it('should save without throwing an exception', function(){
function trySave() {
_sheet.save( 'tests/sandbox/jane-doe.json' );
}
trySave.should.not.Throw();
});
it('should not be modified after saving', function() {
var savedSheet = new JRSResume().open( 'tests/sandbox/jane-doe.json' );
_sheet.stringify().should.equal( savedSheet.stringify() )
});
it('should validate against the JSON Resume schema', function() {
var result = _sheet.isValid();
// var schemaJson = require('../src/core/resume.json');
// var validate = validator( schemaJson, { verbose: true } );
// var result = validate( JSON.parse( _sheet.imp.raw ) );
result || console.log("\n\nOops, resume didn't validate. " +
"Validation errors:\n\n", _sheet.basics.imp.validationErrors, "\n\n");
result.should.equal( true );
});
});

View File

@ -1,52 +0,0 @@
var chai = require('chai')
, expect = chai.expect
, should = chai.should()
, path = require('path')
, _ = require('underscore')
, FRESHResume = require('../src/core/fresh-resume')
, FCMD = require( '../src/fluentcmd')
, validator = require('is-my-json-valid')
, COLORS = require('colors');
chai.config.includeStack = false;
describe('Testing themes', function () {
var _sheet;
COLORS.setTheme({
title: ['white','bold'],
info: process.platform === 'win32' ? 'gray' : ['white','dim'],
infoBold: ['white','dim'],
warn: 'yellow',
error: 'red',
guide: 'yellow',
status: 'gray',//['white','dim'],
useful: 'green',
});
function genTheme( themeName ) {
it( themeName.toUpperCase() + ' theme should generate without throwing an exception', function () {
function tryOpen() {
var src = ['node_modules/FRESCA/exemplar/jane-doe.json'];
var dst = ['tests/sandbox/hello-world/resume.all'];
var opts = {
theme: themeName,
format: 'FRESH',
prettify: true,
silent: false
};
FCMD.verbs.build( src, dst, opts );
}
tryOpen.should.not.Throw();
});
}
genTheme('hello-world');
genTheme('compact');
genTheme('modern');
genTheme('minimist');
genTheme('awesome');
});