1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2025-05-15 10:07:07 +01:00

Compare commits

..

207 Commits

Author SHA1 Message Date
ab6e7ee1a0 fix: create command glitch 2018-02-14 11:23:51 -05:00
8cccd2ffbb chore: reset rasterize.js 2018-02-14 11:23:15 -05:00
c6efdeca05 chore: decaffeinate: remove generated dist/ folder 2018-02-14 10:13:48 -05:00
42d249b407 chore: decaffeinate: fix eslint violations 2018-02-14 10:02:44 -05:00
8a46d642e5 chore: decaffeinate: convert error.coffee and 58 other files to JS 2018-02-14 09:56:31 -05:00
b7cd01597e chore: decaffeinate: rename error.coffee and 58 other files from .coffee to .js 2018-02-14 09:55:12 -05:00
73c5674af8 style: fix comment typo 2018-02-13 20:39:12 -05:00
b077ff42e4 chore: add shrinkwrap 2018-02-12 07:33:02 -05:00
1bc3485812 chore: set HackMyResume version to 1.9.0-beta 2018-02-12 07:11:28 -05:00
7597eda198 docs: merge enhancements 2018-02-12 07:05:49 -05:00
3badb46ae4 docs: update README images 2018-02-12 06:48:26 -05:00
30affe351d chore: upate dot files 2018-02-12 05:28:02 -05:00
407f9f8cd7 style: fix eslint violations 2018-02-12 04:01:00 -05:00
922c1968ca chore: add build-time eslint support 2018-02-12 03:34:55 -05:00
093a2c6a56 docs: update CHANGELOG for 1.9.0 2018-02-12 02:47:03 -05:00
031b666b0a Merge branch 'dev' into docs-2.0 2018-02-12 00:35:45 -05:00
033b29fd3a test: remove Node 4 and 5 from Travis 2018-02-12 00:16:51 -05:00
c4f7350528 chore: update project dependencies 2018-02-12 00:05:29 -05:00
7144126175 feat: improve help behavior 2018-02-11 08:13:13 -05:00
a5739f337f chore: move use.txt to help/ folder 2018-02-10 13:28:42 -05:00
98f20c368c feat: introduce HELP command
Add support for a "HELP" verb in the HackMyResume CLI, allowing separate help
pages for each HackMyResume command per #205. The following command invocations
are recognized.

    hackmyresume help
    hackmyresume help build
    hackmyresume help new
    hackmyresume help convert
    hackmyresume help analyze
    hackmyresume help validate
    hackmyresume help peek
    hackmyresume help help
2018-02-10 13:03:52 -05:00
2281b4ea7f chore: bump fresh-jrs-converter to 1.0.0 2018-02-10 03:02:22 -05:00
7196bff27c feat: support JSON Resume edge schema 2018-02-10 01:10:20 -05:00
7cfdb95a04 feat: convert: support multiple JRS versions 2018-02-09 21:34:24 -05:00
58fe46dc83 feat: introduce FRESH version regex 2018-02-09 21:32:44 -05:00
17e5c6c172 style: notate an issue in skills coalescing func 2018-02-09 00:17:10 -05:00
ea3c72845e chore: bump fresh-test-resumes to 0.9.2 explicit 2018-02-09 00:08:41 -05:00
06805578a2 chore: introduce fresh-resume-validator dependency 2018-02-09 00:07:37 -05:00
20815d7eff fix: add missing require() 2018-02-09 00:06:07 -05:00
8648befcdd feat: introduce stringOrObject & linkMD helpers 2018-02-07 23:35:05 -05:00
c08c5f0fa3 feat: introduce two skill-related helpers 2018-02-07 05:55:27 -05:00
38a073b09a feat: improve template helper wiring 2018-02-07 05:49:02 -05:00
2346562428 fix: remove extraneous log statement 2018-02-06 08:24:18 -05:00
2bf5bb72cf fix: prevent broken XML in Word docs 2018-02-06 08:22:31 -05:00
7262578c81 feat: allow standalone FRESH themes to inherit 2018-02-05 23:43:38 -05:00
66f3cb15c9 style: remove unused var 2018-02-05 23:41:40 -05:00
6f07141b0d test: remove image files from freeze test 2018-02-05 00:06:14 -05:00
dc88073bcc test: reactivate Awesome theme and ice check 2018-02-04 23:35:42 -05:00
8c81a54565 fix: resolve issues around @@@@ characters in dates
Simplify resume freezing; avoid transformations on foreign fields. Fixes #198
but needs followup to allow users to specify how and when freezing, encoding,
and transformations occur.
2018-02-04 23:06:34 -05:00
8dca5b76e7 refactor: remove AbstractResume base class
(1) AbstractResume adds complexity without contributing utility. There's not
really a clean "class" abstraction in JavaScript to begin with; CoffeeScript
classes, as nice as they are syntactically, occlude the issue even further.

(2) AbstractResume currently functions as a container for exactly two functions
which arguably should live outside the resume class anyway.
2018-02-04 22:49:58 -05:00
2767b16b47 test: use direct dependency for fresh-resume-underscore 2018-02-04 05:34:47 -05:00
f1343add71 test: remove hard-coded submodule path 2018-02-04 04:44:47 -05:00
81d9d5f157 test: add fresh-theme-underscore to suite 2018-02-04 04:06:49 -05:00
caca653666 style: remove unnecessary expression 2018-02-04 01:43:51 -05:00
55196c2766 fix: prevent weird characters in date fields 2018-02-04 01:13:02 -05:00
00067d012a fix: correctly replace frozen fields in JRS-themed resumes 2018-02-03 16:15:17 -05:00
9da69c3310 Merge branch 'dev' into docs-2.0 2018-02-03 04:36:56 -05:00
02f0af1ff8 chore: update HackMyResume version to 1.9.0 2018-02-03 04:35:07 -05:00
b1515fc656 docs: update fresh-resume-schema link 2018-02-03 04:31:22 -05:00
ba719166f7 test: replace fresca with fresh-resume-schema 2018-02-02 04:49:26 -05:00
db6ec47dcc chore: update stale JavaScript 2018-02-02 04:48:28 -05:00
f53c316ecb chore: replace fresca with fresh-resume-schema 2018-02-02 03:42:50 -05:00
e889481ad8 chore: bump HackMyResume version to 2.0.0
With this upcoming release we've introduced potentially breaking functionality
so a major version bump is indicated. Additionally, it's been over a year since
the last HackMyResume release; a major version bump allows us to accept some
fruitful breakage, giving us a clean basis as we move forward on new features.
2018-02-02 02:56:59 -05:00
2b31f5bb58 docs: capture readme & changelog updates (interim) 2018-02-02 02:33:13 -05:00
7912ec9ef5 chore: add fresh-test-themes dependency 2018-02-01 22:02:50 -05:00
e6e0b135ed style: clean up comments in jrs-theme.coffee 2018-02-01 19:22:17 -05:00
54d056c4b7 fix: exclude theme.json from interrogation 2018-02-01 17:56:43 -05:00
157a2a6145 chore: bump fresh-themes to 0.16.0-beta 2018-02-01 09:37:20 -05:00
688767d415 feat: improve ad hoc theme loading 2018-02-01 07:20:12 -05:00
1dbb78c53f feat: improve custom theme helper registration 2018-02-01 07:00:59 -05:00
9c096541ce feat: allow FRESH themes to specify base folder
Currently, FRESH themes contain a `src` folder that contains theme artifacts.
This commit allows the theme to specify a different folder (including "." or
""), supporting arbitrary folder structures.
2018-02-01 06:52:06 -05:00
5161a3a823 feat: include private fields during convert 2018-02-01 06:44:07 -05:00
76a386c9df chore: bump fresh-test-resumes version to 0.9.1 2018-02-01 06:22:53 -05:00
7d78deec5f test: replace 'resumes/' folder with fresh-test-resumes 2018-02-01 05:58:35 -05:00
a265fb633d chore: add 'glob' dependency 2018-01-31 22:01:28 -05:00
069506e86d feat: support custom theme helpers 2018-01-31 21:11:21 -05:00
7f656175f0 chore: bump jrs-fresh-converter to 0.2.3 2018-01-31 16:19:03 -05:00
94fc54174c refactor: remove unnecessary var 2018-01-31 16:17:46 -05:00
231357badc [fix] Replace legacy theme detection code. 2018-01-31 16:00:09 -05:00
fde2146a0b [fix] Private fields: resolve off-by-one error [2]. 2018-01-31 15:22:15 -05:00
c6adab7f9e [fix] Private fields: Resolve off-by-one error. 2018-01-31 00:10:37 -05:00
7f7c936897 [chore] Test: Increase resume generation timeout. 2018-01-30 13:04:08 -05:00
a9e35203c2 [style] Tighten syntax in html-pdf-cli-generator.coffee|js. 2018-01-30 12:40:17 -05:00
c913de4bf7 CONVERT: Improve command consistency. 2018-01-30 02:34:58 -05:00
6b125ed907 Support no-escape option for Handlebars themes. 2018-01-29 05:21:46 -05:00
17259cedbf Detect bad option files supplied via --options. 2018-01-29 02:04:00 -05:00
12a14dadeb Merge branch 'master' into dev 2018-01-28 22:38:08 -05:00
35fb2f5dac Fix Travis build issues. (#204) 2018-01-28 22:34:05 -05:00
097e81caf8 Merge pull request #191 from ryneeverett/theme-helpers
Register handlebars helpers in themes. Fix #158.
2018-01-27 17:44:36 -05:00
6adf195281 Travis: Update Node.js versions. 2018-01-26 10:02:26 -05:00
79c304a08b Update text exemplars. 2018-01-25 15:04:49 -05:00
394d5cf821 Merge branch 'master' into dev 2018-01-25 14:48:35 -05:00
6092f985f2 Merge branch 'dev' of https://github.com/hacksalot/HackMyResume into dev 2018-01-25 14:48:14 -05:00
2c26c21144 Bump fresh resume versions. 2018-01-25 14:47:22 -05:00
6bd0b817af Merge pull request #177 from ael-code/pdf-margins
instruct wkhtmltopdf to insert top/bottom margins
2018-01-25 13:05:35 -05:00
97fe171775 Merge branch 'master' into pdf-margins 2018-01-25 13:05:24 -05:00
9718c652ab Merge pull request #164 from jonathanGB/master
little fix in the "Use" base case
2018-01-25 12:51:06 -05:00
9aad6d6138 Merge pull request #183 from peternowee/fix-issue165
Update fresh-resume-starter dependancy to 0.3.x
2018-01-25 12:44:34 -05:00
d25f8d0f97 Merge pull request #186 from ryneeverett/same-value-dateRange
When date ranges are identical, only show one.
2018-01-25 12:43:48 -05:00
d2d9039abb Merge pull request #189 from ryneeverett/weasyprint
Add WeasyPrint pdf generator support.
2018-01-25 12:10:25 -05:00
3dc6ff2158 Merge pull request #184 from ryneeverett/noescape
Don't do html escaping. Fix #157.
2018-01-25 02:27:32 -05:00
36f5f46753 Merge pull request #192 from peternowee/fix-example-options-file
Mark options file example JSON and remove comments
2018-01-24 23:46:50 -05:00
8cc6334cd1 Bump FRESCA dependency to 0.6.1. 2018-01-24 21:30:19 -05:00
b7ef40709e Update copyright notice. 2018-01-24 21:29:47 -05:00
efe97ad793 Update tests.
Knock the dust off the HackMyResume test suite.
2018-01-24 21:29:24 -05:00
a243354044 Change admin email to admin@fluentdesk.com. 2018-01-24 21:26:45 -05:00
92c477e139 Mark options file example JSON and remove comments
The options file is JSON, not JavaScript, and JSON does not allow
comments. An options file with comments as shown in the current
example in README.md will fail to load.

This commit removes the comments. They do not seem important enough to
place them elsewhere.
2017-02-17 12:28:56 +01:00
ec591b9432 Register handlebars helpers in themes. Fix #158.
Try to register all javascript files found in themes as handlebars
helpers.

Note that, unlike all other theme files currently, format directories
are ignored. I don't think there's a use case for format-specific
helpers, and this gives theme developers the flexibility to put them
either in top level files or organize them in subdirectories however
they see fit.

Note also that the theme format seems to be primarily documented in
<https://github.com/fresh-standard/fresh-themes>. This newly recognized
theme file type should be documented there should this branch be merged.
2017-01-20 22:53:16 -05:00
419c935d82 Add weasyprint pdf generator support.
Note: This is based off
<https://github.com/hacksalot/HackMyResume/pull/185> because I changed
the generator expected arguments in that branch.
2017-01-16 00:15:04 -05:00
d31f6caf50 When date ranges are identical, only show one. 2017-01-06 22:37:08 -05:00
7e2a3c3e7e Add option to pass wkhtmltopdf options. Fix #176.
It seems that some time in the last couple years wkhtmltopdf's default
margins were changed from '10mm' to zero. As an alternative to #177,
this PR adds an option to pass in arbitrary wkhtmltopdf long arguments
and sets the default top and bottom margin to '10mm'.
2017-01-06 19:10:27 -05:00
406d3358eb Don't do html escaping. Fix #157. 2016-12-14 21:14:45 -05:00
98ef625d7b Update fresh-resume-starter dependancy to 0.3.x
Fixes issue 165 (Specifying JRS as format resolves in undefined).
2016-11-26 07:29:44 +01:00
be8a7a8361 instruct wkhtmltopdf to insert top/bottom margins
added `--margin-top 10` and `-margin-bottom 10` options in the invocation of `wkhtmltopdf`.
2016-10-21 20:03:54 +02:00
37720677f0 little fix in the "Use" base case 2016-06-27 22:04:57 -04:00
0cd59416b8 Remove unused dependency. 2016-04-02 15:01:28 -04:00
abdeeb4385 Introduce placeholder logo (interim).
Add quick 'n dirty .AI and .PNG assets for interim logo; link through
LFS.
2016-02-16 21:08:35 -05:00
bb7944bee7 Update resume exemplars w/ --private. 2016-02-15 16:56:00 -05:00
9de1156144 Update contributors.
CC @daniele-rapagnani.
2016-02-15 16:48:05 -05:00
9ae2703eeb Bump version to 1.9.0. 2016-02-15 16:39:37 -05:00
a3ed56dd15 Update JS.
Keep dist/ updated with src/ for the CLI and require() folks.
(package.json goes off dist).
2016-02-15 16:38:01 -05:00
214c53a3ab Merge pull request #142 from daniele-rapagnani/feature-private-fields
Implements private fields as requested in #88
2016-02-15 15:52:29 -05:00
ba6b8d45f5 Reverted the compiled JS to avoid merge conflicts 2016-02-14 22:21:06 +01:00
3c166a21a0 Removed the forced private option from the CONVERT verb as it is now the default behaviour 2016-02-14 22:10:30 +01:00
fe46d15031 The ANALYZE command now excludes private fields by default for consistency. 2016-02-14 21:53:10 +01:00
664eea752f parseJSON has been modified to always include private fields if not otherwise instructed. This is to ensure back-compatibility. The BUILD command instead, excludes private fields by default 2016-02-14 21:50:13 +01:00
fed59b704e Implemented private fields that can be included or excluded with cli switch 2016-02-14 19:15:47 +01:00
3cf850ea0e Update test exemplars. 2016-02-14 05:32:33 -05:00
1b0bc87b60 Update changelog and version. 2016-02-14 04:54:44 -05:00
5d3b993737 Bump fresh-themes to 0.15.1-beta.
Not 100% necessary given "^" but support naive stripping of the "^"
decorator.
2016-02-14 04:32:39 -05:00
917fd8e3f3 Refactor helpers.
Rebind Handlebars helpers to drop the pesky options hash for standalone
helpers that don't need it. Move block helpers (which do need the
Handlebars options/context) to a separate file for special handling.
2016-02-14 04:10:23 -05:00
6ac2cd490b Bump fresh-test-resumes to 0.7.0. 2016-02-13 23:45:53 -05:00
8100190978 Bump fresh-themes to 0.15.0-beta. 2016-02-13 22:54:43 -05:00
7c36ff8331 Introduce "date" helper. 2016-02-13 22:54:07 -05:00
255a518565 Set test timeout to 30 seconds.
Most themes should generate in < 1s but allow up to 30 seconds for
network latency when opening a remote file, or for fetching remote
resources (CSS, JS, etc) during a local build.
2016-02-13 20:42:03 -05:00
2d595350c6 Escape LaTeX during generation. 2016-02-13 20:40:17 -05:00
ca92d41d9e Numerous fixes. 2016-02-13 16:08:45 -05:00
3f8e795c61 Fix generation glitches.
Fix output file name glitch, writing CSS files to destination folder,
and an issue where the process would evaporate before PDF/PNG generation
could complete.
2016-02-13 03:27:11 -05:00
9927e79900 Clean up CoffeeScript. 2016-02-13 00:40:10 -05:00
dbef9f0a35 Improve VALIDATE error handling. 2016-02-13 00:11:52 -05:00
c889664c31 More VALIDATE fixups. 2016-02-12 23:47:08 -05:00
7a60cd0bab Fixup VALIDATE command.
Introduce MISSING and UNKNOWN states alongside BROKEN, VALID, and
INVALID and fix regressions introduced in previous refactorings.
2016-02-12 22:49:56 -05:00
964350d3c7 Bump fresh-jrs-converter to 0.2.2.
Technically the "^0.2.1" implies v0.2.2 but eventually we'll drop the
"^" and use shrinkwrapped versions in dev, so explicitly bump for now.
2016-02-12 21:42:50 -05:00
b57d9f05af jsHint: Allow == null.
Relax jsHint's barbaric and antiquated default behavior on triple-equals
null comparisons. Allow expressive, precise, and subtle expressions such
as CoffeeScript's use of "== null" for testing against null OR undefined
under the existential operator.
2016-02-12 17:42:48 -05:00
b26799f9fc Improve JSON error handling.
Add support for detection of invalid line breaks in JSON string values.
Fixes #137. Could be improved to fetch the column number and drop the
messy grabbing of the line number from the exception message via regex,
but currently the "jsonlint" library (not to be confused with
"json-lint") only emits an error string. Since this is also the library
that drives http://jsonlint.com, we'll accept the messy regex in return
for more robust error checking when our default json-lint path fails.

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

https://github.com/hacksalot/HackMyAPI
2016-01-24 09:55:04 -05:00
122 changed files with 15567 additions and 5675 deletions

17
.eslintrc.yml Normal file
View File

@ -0,0 +1,17 @@
env:
es6: true
node: true
extends: 'eslint:recommended'
rules:
# indent:
# - error
# - 4
linebreak-style:
- error
- unix
quotes:
- error
- single
semi:
- error
- always

6
.gitattributes vendored
View File

@ -1,9 +1,11 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto
*.js text eol=lf *.js text eol=lf
*.json text eol=lf *.json text eol=lf
# Standard to msysgit # Standard to msysgit
*.doc diff=astextplain *.doc diff=astextplain
*.DOC diff=astextplain *.DOC diff=astextplain
*.docx diff=astextplain *.docx diff=astextplain
@ -14,3 +16,7 @@
*.PDF diff=astextplain *.PDF diff=astextplain
*.rtf diff=astextplain *.rtf diff=astextplain
*.RTF diff=astextplain *.RTF diff=astextplain
# Git LFS
*.ai filter=lfs diff=lfs merge=lfs -text

2
.gitignore vendored
View File

@ -1,9 +1,9 @@
node_modules/ node_modules/
tests/sandbox/
doc/ doc/
docs/ docs/
local/ local/
npm-debug.log npm-debug.log
*.map
# Emacs detritus # Emacs detritus
# -*- mode: gitignore; -*- # -*- mode: gitignore; -*-

13
.npmignore Normal file
View File

@ -0,0 +1,13 @@
assets/
test/
doc/
.travis.yml
.eslintrc.yml
Gruntfile.js
.gitattributes
ROADMAP.md
BUILDING.md
CONTRIBUTING.md
CHANGELOG.md
FAQ.md
*.map

View File

@ -1,9 +1,23 @@
sudo: required
before_install:
# Prevents a shared object .so error when running wkhtmltopdf on certain
# platforms (e.g., vanilla Ubuntu 16.04 LTS). Not necessary on current Travis.
# - sudo apt-get install libxrender1
install:
# Install & link HackMyResume
- npm install && npm link
# Download and extract the latest wkhtmltopdf binaries
- mkdir tmp && wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.4/wkhtmltox-0.12.4_linux-generic-amd64.tar.xz -O tmp/wk.tar.xz
- tar -xf tmp/wk.tar.xz -C ./tmp
# Copy wkhtmltopdf binaries to /usr/bin (also makes them path-accessible)
- sudo cp -R ./tmp/wkhtmltox/bin/* /usr/bin/
# Now you can invoke "wkhtmltopdf" and "wkhtmltoimage" safely in tests.
- wkhtmltopdf -V
- wkhtmltoimage -V
language: node_js language: node_js
node_js: node_js:
- "0.10" - "6"
- "0.11" - "7"
- "0.12" - "8"
- "4.0" - "9"
- "4.1" - "lts/*"
- "4.2"
- "5.0"

50
BUILDING.md Normal file
View File

@ -0,0 +1,50 @@
Building
========
*See [CONTRIBUTING.md][contrib] for more information on contributing to the
HackMyResume or FluentCV projects.*
HackMyResume is a standard Node.js command line app implemented in a mix of
CoffeeScript and JavaScript. Setting up a build environment is easy:
## Prerequisites ##
1. OS: Linux, OS X, or Windows
2. Install [Node.js][node] and [Grunt][grunt].
## Set up a build environment ###
1. Fork [hacksalot/HackMyResume][hmr] to your GitHub account.
2. Clone your fork locally.
3. From within the top-level HackMyResume folder, run `npm install` to install
project dependencies.
4. Create a new branch, based on the latest HackMyResume `dev` branch, to
contain your work.
5. Run `npm link` in the HackMyResume folder so that the `hackmyresume` command
will reference your local installation (you may need to
`npm uninstall -g hackmyresume` first).
## Making changes
1. HackMyResume sources live in the [`/src`][src] folder.
2. When you're ready to submit your changes, run `grunt test` to run the HMR
test suite. Fix any errors that occur.
3. Commit and push your changes.
4. Submit a pull request targeting the HackMyResume `dev` branch.
[node]: https://nodejs.org/en/
[grunt]: http://gruntjs.com/
[hmr]: https://github.com/hacksalot/HackMyResume
[src]: https://github.com/hacksalot/HackMyResume/tree/master/src
[contrib]: https://github.com/hacksalot/HackMyResume/blob/master/CONTRIBUTING.md

View File

@ -1,5 +1,199 @@
CHANGELOG CHANGELOG
========= =========
## v1.9.0-beta
*Welcome to the first new version of HackMyResume in over a year. The purpose of
this release is to gather feature enhancements and bug fixes collected over the
past 18 months as we reorganize, rebrand, and prepare for the 2.0 release.*
### Added
- Support for **private resume fields**. Mark any non-leaf node in your resume
JSON with the `private` property and it will be omitted from outbound resumes.
```json
"employment": {
"history": [
{
"employer": "Acme Real Estate"
},
{
"employer": "Area 51 Alien Research Laboratory",
"private": true
},
{
"employer": "H&R Block"
}
]
}
```
- Support for **PDF generation through WeasyPrint** in addition to the
existing support for wkhtmltopdf and PhantomJS.
- Theme authors can now develop and package **custom Handlebars theme helpers**
via the `helpers` key of the `theme.json` file (FRESH themes only) (#158).
- Help system has been updated with a `HELP` command and dedicated help pages
for each command.
- Theme authors can **relocate theme assets** with the `baseFolder` property in
the FRESH `theme.json`.
- HackMyResume will now **validate the options file** (if any) loaded with `-o`
or `--options` and warn the user if necessary.
- Ability to **disable Handlebars encoding/escaping** of resume fields with
`--no-escape`.
- Introduced the [fresh-test-themes][ftt] project as a repository for simple,
test-only resume themes in the FRESH format.
### Changed
- Dropped support for Node 4 and 5. HackMyResume officially runs on Node 6+.
- The FRESCA project has been renamed to [fresh-resume-schema][fresca]. FRESCA
is still the nickname.
- The HackMyResume web page has moved to https://fluentdesk.com/hackmyresume.
### Fixed
- Fixed an issue that would cause the `convert` command to detect the inbound
resume type (FRESH or JRS) incorrectly (#162).
- Fixed an issue where generating new JRS resumes would cause a `null` or
`undefined` error (#165).
- Fixed an issue preventing mixed-case themes from being loaded (#172).
- Fixed an issue requiring JSON Resume themes contain `json-resume-theme` in the
theme path (#173).
- Fixed an issue that would cause strange `@@@@` characters to appear in
generated resumes (#207, #168, #198).
- Fixed an issue that would cause resume generation to hang after a JSON Resume
themed resume was generated (#182).
- Fixed an issue that would cause nothing to be generated for Markdown (.md)
formats (#179).
- Fixed an issue that would prevent a JRS resume from being converted to FRESH
via the `convert` command (#180).
- Fixed an issue that would cause broken styling for JSON Resume themes (#155).
### Internal
- Tests: fixed resume duration tests (#181).
- Style: move to
## v1.8.0
### Added
- Updated `Awesome` theme to latest version of [Awesome-CV][acv].
- Introduced new theme helpers: `pad`, `date`.
### Fixed
- Fixed an issue where the `Awesome` theme wouldn't correctly generate LaTeX
outputs (#138).
- Emit a line number for syntax errors around embedded newlines in JSON strings
(#137).
- Fix several PDF / PNG generation errors (#132, others).
- Display a more helpful error message when attempting to generate a PDF or PNG
on a machine where PhantomJS and/or wkhtmltopdf are either not installed or
not path-accessible.
- Fixed an issue that would cause long-running PDF/PNG generation to fail in
certain environments.
- Fixed an issue involving an unhelpful spawn-related exception (#136).
### Internal
- JSHint will no longer gripe at the use of `== null` and `!= null` in
CoffeeScript transpilation.
- Introduced [template-friendly Awesome-CV fork][awefork] to isolate template
expansion logic & provide better durability for HackMyResume's `awesome` theme.
- Fixed a couple temporary regressions (#139, #140) on the dev branch.
- Additional tests.
- Minor breaking HackMyResume API changes.
## v1.7.4
### Added
- [Build instructions](https://github.com/hacksalot/HackMyResume/blob/master/BUILDING.md).
### Changed
- More precise date handling.
### Fixed
- Issue with incomplete PDF generation (#127).
- Issue with building JSON Resume themes (#128).
- Issue with generating `.json` output format by itself (#97).
## v1.7.3
### Fixed
- Issue with generated PDFs being chopped off and displaying a mysterious sequence of numbers of unknown and possibly alien origin (#127).
- Unsightly border on Modern:PDF.
- Modern|Positive:PDF formats now correctly reference their PDF-specific CSS files.
- `Incorrect helper use` warning in Positive:DOC.
## v1.7.2
### Changed
- Interim release supporting FluentCV Desktop.
### Internal
- Moved [HackMyCore](https://github.com/hacksalot/HackMyCore) dependency to
submodule.
## v1.7.1
### Changed
- Caffeinate. CoffeeScript now used throughout
[HackMyResume](https://github.com/hacksalot/HackMyResume) and
[HackMyCore](https://github.com/hacksalot/HackMyCore); generated JavaScript
lives in `/dist`.
### Fixed
- Issue with generating a single PDF with the `.pdf` extension (#99).
## v1.7.0
### Changed
- [Internal] Relocated HMR processing code to the
[HackMyCore](https://github.com/hacksalot/HackMyCore) project. Shouldn't affect
normal use.
## v1.6.0 ## v1.6.0
### Major Improvements ### Major Improvements
@ -302,5 +496,8 @@ theme.
[i92]: https://github.com/hacksalot/HackMyResume/issues/92 [i92]: https://github.com/hacksalot/HackMyResume/issues/92
[i101]: https://github.com/hacksalot/HackMyResume/issues/101 [i101]: https://github.com/hacksalot/HackMyResume/issues/101
[i111]: https://github.com/hacksalot/HackMyResume/issues/111 [i111]: https://github.com/hacksalot/HackMyResume/issues/111
[fresca]: https://github.com/fluentdesk/FRESCA [fresca]: https://github.com/fresh-standard/fresh-resume-schema
[themes]: https://github.com/fluentdesk/fresh-themes [themes]: https://github.com/fresh-standard/fresh-themes
[awefork]: https://github.com/fluentdesk/Awesome-CV
[acv]: https://github.com/posquit0/Awesome-CV
[ftt]: https://github.com/fresh-standard/fresh-test-themes

View File

@ -4,17 +4,11 @@ Contributing
*Note: HackMyResume is also available as [FluentCV][fcv]. Contributors are *Note: HackMyResume is also available as [FluentCV][fcv]. Contributors are
credited in both.* 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 ## How To Contribute
*See [BUILDING.md][building] for instructions on setting up a HackMyResume
development environment.*
1. Optional: [**open an issue**][iss] identifying the feature or bug you'd like 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 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 HackMyResume without clearing it with us — but helps avoid duplication of work
@ -25,7 +19,7 @@ similar; call it whatever you like) to perform your work in.
4. **Install dependencies** by running `npm install` in the top-level 4. **Install dependencies** by running `npm install` in the top-level
HackMyResume folder. HackMyResume folder.
5. Make your **commits** as usual. 5. Make your **commits** as usual.
6. **Verify** your changes locally with `npm test`. 6. **Verify** your changes locally with `grunt test`.
7. **Push** your commits. 7. **Push** your commits.
7. **Submit a pull request** from your feature branch to the HackMyResume `dev` 7. **Submit a pull request** from your feature branch to the HackMyResume `dev`
branch. branch.
@ -39,7 +33,7 @@ HackMyResume is currently maintained by [hacksalot][ha] with assistance from
or internal inquiries to: or internal inquiries to:
``` ```
admin@hackmyresume.com admin@fluentdesk.com
``` ```
You can reach hacksalot directly at: You can reach hacksalot directly at:
@ -48,7 +42,7 @@ You can reach hacksalot directly at:
hacksalot@indevious.com hacksalot@indevious.com
``` ```
Thanks! See you out there in the trenches. Thanks for your interest in the HackMyResume project.
[fcv]: https://github.com/fluentdesk/fluentcv [fcv]: https://github.com/fluentdesk/fluentcv
[flow]: https://guides.github.com/introduction/flow/ [flow]: https://guides.github.com/introduction/flow/
@ -56,3 +50,4 @@ Thanks! See you out there in the trenches.
[ha]: https://github.com/hacksalot [ha]: https://github.com/hacksalot
[th]: https://github.com/tomheon [th]: https://github.com/tomheon
[awesome]: https://github.com/hacksalot/HackMyResume/graphs/contributors [awesome]: https://github.com/hacksalot/HackMyResume/graphs/contributors
[building]: https://github.com/hacksalot/HackMyResume/blob/master/BUILDING.md

View File

@ -17,54 +17,36 @@ module.exports = function (grunt) {
all: { src: ['test/*.js'] } all: { src: ['test/*.js'] }
}, },
jsdoc : { clean: {
dist : { test: ['test/sandbox']
src: ['src/**/*.js'],
options: {
private: true,
destination: 'doc'
}
}
}, },
clean: ['test/sandbox'], eslint: {
target: ['Gruntfile.js', 'src/**/*.js', 'test/*.js']
yuidoc: {
compile: {
name: '<%= pkg.name %>',
description: '<%= pkg.description %>',
version: '<%= pkg.version %>',
url: '<%= pkg.homepage %>',
options: {
paths: 'src/',
//themedir: 'path/to/custom/theme/',
outdir: 'docs/'
}
}
},
jshint: {
options: {
laxcomma: true,
expr: true
},
all: ['Gruntfile.js', 'src/**/*.js', 'test/*.js']
} }
}; };
grunt.initConfig( opts ); grunt.initConfig( opts );
grunt.loadNpmTasks('grunt-simple-mocha'); grunt.loadNpmTasks('grunt-simple-mocha');
grunt.loadNpmTasks('grunt-contrib-yuidoc'); grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-jsdoc');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-clean');
grunt.registerTask('test', 'Test the HackMyResume library.', // Use 'grunt test' for local testing
function( config ) { grunt.task.run(['clean','jshint','simplemocha:all']); }); grunt.registerTask('test', 'Test the HackMyResume application.',
grunt.registerTask('document', 'Generate HackMyResume library documentation.', function() {
function( config ) { grunt.task.run( ['jsdoc'] ); }); grunt.task.run(['clean:test','build','eslint','simplemocha:all']);
grunt.registerTask('default', [ 'test', 'jsdoc' ]); }
);
// Use 'grunt build' to build HMR
grunt.registerTask('build', 'Build the HackMyResume application.',
function() {
grunt.task.run( ['eslint'] );
}
);
// Default task does everything
grunt.registerTask('default', [ 'test' ]);
}; };

View File

@ -1,7 +1,7 @@
The MIT License The MIT License
=============== ===============
Copyright (c) 2016 hacksalot (https://github.com/hacksalot) Copyright (c) 2015-2018 hacksalot (https://github.com/hacksalot)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

218
README.md
View File

@ -1,16 +1,17 @@
HackMyResume HackMyResume
============ ===
[![Latest release][img-release]][latest-release] [![Latest release][img-release]][latest-release]
[![Build status (MASTER)][img-master]][travis-url-master] [![Build status (MASTER)][img-master]][travis-url-master]
[![Build status (DEV)][img-dev]][travis-url-dev] [![Build status (DEV)][img-dev]][travis-url-dev]
[![Join the chat at https://gitter.im/hacksalot/HackMyResume][badge]][gh]
*Create polished résumés and CVs in multiple formats from your command line or *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, 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 plain text, and other arbitrary formats. Fight the power, save trees. Compatible
with [FRESH][fresca] and [JRS][6] resumes.* with [FRESH][fresca] and [JRS][6] resumes.*
![](assets/hackmyresume.cli.1.6.0.png) ![](assets/hmr_build.png)
HackMyResume is a dev-friendly, local-only Swiss Army knife for resumes and CVs. HackMyResume is a dev-friendly, local-only Swiss Army knife for resumes and CVs.
Use it to: Use it to:
@ -26,6 +27,8 @@ metrics.
HackMyResume is built with Node.js and runs on recent versions of OS X, Linux, HackMyResume is built with Node.js and runs on recent versions of OS X, Linux,
or Windows. View the [FAQ](FAQ.md). or Windows. View the [FAQ](FAQ.md).
![](assets/hmr_analyze.png)
## Features ## Features
- OS X, Linux, and Windows. - OS X, Linux, and Windows.
@ -61,9 +64,9 @@ Alternately, install the latest bleeding-edge version (updated daily):
HackMyResume tries not to impose a specific PDF engine requirement on HackMyResume tries not to impose a specific PDF engine requirement on
the user, but will instead work with whatever PDF engines you have installed. the user, but will instead work with whatever PDF engines you have installed.
Currently, HackMyResume's PDF generation requires either [Phantom.js][2] or Currently, HackMyResume's PDF generation requires one of [Phantom.js][2],
[wkhtmltopdf][3] to be installed on your system and the `phantomjs` and/or [wkhtmltopdf][3], or [WeasyPrint][11] to be installed on your system and the
`wkhtmltopdf` binaries to be accessible on your PATH. This is an optional corresponding binary to be accessible on your PATH. This is an optional
requirement for users who care about PDF formats. If you don't care about PDF requirement for users who care about PDF formats. If you don't care about PDF
formats, skip this step. formats, skip this step.
@ -90,7 +93,7 @@ Then when you're ready to generate your resume, just reference the location of
the theme folder as you installed it: the theme folder as you installed it:
```bash ```bash
hackmyresume BUILD resume.json TO out/resume.all -t node_modules/jsonresume-theme-classy 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 Note: You can use install themes anywhere on your file system. You don't need a
@ -107,26 +110,26 @@ Use it when you need to submit, upload, print, or email resumes in specific
formats. formats.
```bash ```bash
# hackmyresume BUILD <INPUTS...> TO <OUTPUTS...> [-t THEME] # hackmyresume build <INPUTS...> TO <OUTPUTS...> [-t THEME]
hackmyresume BUILD resume.json TO out/resume.all hackmyresume build resume.json TO out/resume.all
hackmyresume BUILD r1.json r2.json TO out/rez.html out/rez.md foo/rez.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 ```bash
# hackmyresume NEW <OUTPUTS...> [-f <FORMAT>] # hackmyresume new <OUTPUTS...> [-f <FORMAT>]
hackmyresume NEW resume.json hackmyresume new resume.json
hackmyresume NEW resume.json -f fresh hackmyresume new resume.json -f fresh
hackmyresume NEW r1.json r2.json -f jrs hackmyresume new r1.json r2.json -f jrs
``` ```
- **analyze** inspects your resume for keywords, duration, and other metrics. - **analyze** inspects your resume for keywords, duration, and other metrics.
```bash ```bash
# hackmyresume ANALYZE <INPUTS...> # hackmyresume analyze <INPUTS...>
hackmyresume ANALYZE resume.json hackmyresume analyze resume.json
hackmyresume ANALYZE r1.json r2.json hackmyresume analyze r1.json r2.json
``` ```
- **convert** converts your source resume between FRESH and JSON Resume - **convert** converts your source resume between FRESH and JSON Resume
@ -134,29 +137,29 @@ formats. Use it to convert between the two formats to take advantage of tools
and services. and services.
```bash ```bash
# hackmyresume CONVERT <INPUTS...> TO <OUTPUTS...> # hackmyresume convert <INPUTS...> TO <OUTPUTS...>
hackmyresume CONVERT resume.json TO resume-jrs.json 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 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. Resume schema. Use it to make sure your resume data is sufficient and complete.
```bash ```bash
# hackmyresume VALIDATE <INPUTS...> # hackmyresume validate <INPUTS...>
hackmyresume VALIDATE resume.json hackmyresume validate resume.json
hackmyresume VALIDATE r1.json r2.json r3.json hackmyresume validate r1.json r2.json r3.json
``` ```
- **peek** echoes your resume or any field, property, or object path on your - **peek** echoes your resume or any field, property, or object path on your
resume to standard output. resume to standard output.
```bash ```bash
# hackmyresume PEEK <INPUTS...> [OBJECT-PATH] # hackmyresume peek <INPUTS...> [OBJECT-PATH]
hackmyresume PEEK rez.json # Echo the whole resume hackmyresume peek rez.json # Echo the whole resume
hackmyresume PEEK rez.json info.brief # Echo the "info.brief" field hackmyresume peek rez.json info.brief # Echo the "info.brief" field
hackmyresume PEEK rez.json employment.history[1] # Echo the 1st job hackmyresume peek rez.json employment.history[1] # Echo the 1st job
hackmyresume PEEK rez.json rez2.json info.brief # Compare value hackmyresume peek rez.json rez2.json info.brief # Compare value
``` ```
## Supported Output Formats ## Supported Output Formats
@ -183,7 +186,7 @@ Assuming you've got a JSON-formatted resume handy, generating resumes in
different formats and combinations is easy. Just run: different formats and combinations is easy. Just run:
```bash ```bash
hackmyresume BUILD <INPUTS> <OUTPUTS> [-t theme]. hackmyresume build <inputs> to <outputs> [-t theme].
``` ```
Where `<INPUTS>` is one or more .json resume files, separated by spaces; Where `<INPUTS>` is one or more .json resume files, separated by spaces;
@ -192,19 +195,19 @@ theme (default to Modern). For example:
```bash ```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.) # Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.)
hackmyresume BUILD resume.json TO out/resume.all -t modern hackmyresume build resume.json TO out/resume.all -t modern
# Generate a specific resume format # Generate a specific resume format
hackmyresume BUILD resume.json TO out/resume.html hackmyresume build resume.json TO out/resume.html
hackmyresume BUILD resume.json TO out/resume.pdf hackmyresume build resume.json TO out/resume.pdf
hackmyresume BUILD resume.json TO out/resume.md hackmyresume build resume.json TO out/resume.md
hackmyresume BUILD resume.json TO out/resume.doc hackmyresume build resume.json TO out/resume.doc
hackmyresume BUILD resume.json TO out/resume.json hackmyresume build resume.json TO out/resume.json
hackmyresume BUILD resume.json TO out/resume.txt hackmyresume build resume.json TO out/resume.txt
hackmyresume BUILD resume.json TO out/resume.yml hackmyresume build resume.json TO out/resume.yml
# Specify 2 inputs and 3 outputs # Specify 2 inputs and 3 outputs
hackmyresume 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: You should see something to the effect of:
@ -231,16 +234,16 @@ installed first). To specify a theme when generating your resume, use the `-t`
or `--theme` parameter: or `--theme` parameter:
```bash ```bash
hackmyresume BUILD resume.json TO out/rez.all -t [theme] hackmyresume build resume.json TO out/rez.all -t [theme]
``` ```
The `[theme]` parameter can be the name of a predefined theme OR the path to any The `[theme]` parameter can be the name of a predefined theme OR the path to any
FRESH or JSON Resume theme folder: FRESH or JSON Resume theme folder:
```bash ```bash
hackmyresume BUILD resume.json TO out/rez.all -t modern 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 ../some-folder/my-custom-theme/
hackmyresume BUILD resume.json TO OUT.rez.all -t node_modules/jsonresume-theme-classy hackmyresume build resume.json TO OUT.rez.all -t node_modules/jsonresume-theme-classy
``` ```
FRESH themes are currently pre-installed with HackMyResume. JSON Resume themes FRESH themes are currently pre-installed with HackMyResume. JSON Resume themes
@ -264,7 +267,7 @@ most generic to most specific:
```bash ```bash
# Merge specific.json onto base.json and generate all formats # Merge specific.json onto base.json and generate all formats
hackmyresume BUILD base.json specific.json TO resume.all hackmyresume build base.json specific.json TO resume.all
``` ```
This can be useful for overriding a base (generic) resume with information from This can be useful for overriding a base (generic) resume with information from
@ -275,7 +278,7 @@ resume. Merging follows conventional [extend()][9]-style behavior and there's
no arbitrary limit to how many resumes you can merge: no arbitrary limit to how many resumes you can merge:
```bash ```bash
hackmyresume 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: in1.json
Reading JSON resume: in2.json Reading JSON resume: in2.json
Reading JSON resume: in3.json Reading JSON resume: in3.json
@ -291,7 +294,7 @@ You can specify **multiple output targets** and HackMyResume will build them:
```bash ```bash
# Generate out1.doc, out1.pdf, and foo.txt from me.json. # Generate out1.doc, out1.pdf, and foo.txt from me.json.
hackmyresume BUILD me.json TO out1.doc out1.pdf foo.txt hackmyresume build me.json TO out1.doc out1.pdf foo.txt
``` ```
### Using .all ### Using .all
@ -301,7 +304,7 @@ formats for the given resume. For example, this...
```bash ```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, etc.) # Generate all resume formats (HTML, PDF, DOC, TXT, etc.)
hackmyresume BUILD me.json TO out/resume.all hackmyresume build me.json TO out/resume.all
``` ```
..tells HackMyResume to read `me.json` and generate `out/resume.md`, ..tells HackMyResume to read `me.json` and generate `out/resume.md`,
@ -316,19 +319,21 @@ and formats with the `--pdf none` switch.*
HackMyResume takes a unique approach to PDF generation. Instead of enforcing HackMyResume takes a unique approach to PDF generation. Instead of enforcing
a specific PDF engine on users, HackMyResume will attempt to work with whatever a specific PDF engine on users, HackMyResume will attempt to work with whatever
PDF engine you have installed through the engine's command-line interface (CLI). PDF engine you have installed through the engine's command-line interface (CLI).
Currently that means one or both of... Currently that means any of...
- [wkhtmltopdf][3] - [wkhtmltopdf][3]
- [Phantom.js][3] - [Phantom.js][2]
- [WeasyPrint][11]
..with support for other engines planned in the future. But for now, **one or ..with support for other engines planned in the future. But for now, **one or
both of these engines must be installed and accessible on your PATH in order to more of these engines must be installed and accessible on your PATH in order
generate PDF resumes with HackMyResume**. That means you should be able to to generate PDF resumes with HackMyResume**. That means you should be able to
invoke either of these tools directly from your shell or terminal without error: invoke either of these tools directly from your shell or terminal without error:
```bash ```bash
wkhtmltopdf input.html output.pdf wkhtmltopdf input.html output.pdf
phantomjs script.js input.html output.pdf phantomjs script.js input.html output.pdf
weasyprint input.html output.pdf
``` ```
Assuming you've installed one or both of these engines on your system, you can Assuming you've installed one or both of these engines on your system, you can
@ -336,9 +341,10 @@ tell HackMyResume which flavor of PDF generation to use via the `--pdf` option
(`-p` for short): (`-p` for short):
```bash ```bash
hackmyresume BUILD resume.json TO out.all --pdf phantom hackmyresume build resume.json TO out.all --pdf phantom
hackmyresume BUILD resume.json TO out.all --pdf wkhtmltopdf hackmyresume build resume.json TO out.all --pdf wkhtmltopdf
hackmyresume BUILD resume.json TO out.all --pdf none hackmyresume build resume.json TO out.all --pdf weasyprint
hackmyresume build resume.json TO out.all --pdf none
``` ```
### Analyzing ### Analyzing
@ -347,7 +353,7 @@ HackMyResume can analyze your resume for keywords, employment gaps, and other
metrics. Run: metrics. Run:
```bash ```bash
hackmyresume ANALYZE <my-resume>.json hackmyresume analyze <my-resume>.json
``` ```
Depending on the HackMyResume version, you should see output similar to: Depending on the HackMyResume version, you should see output similar to:
@ -444,7 +450,7 @@ resumes, use the `validate` command:
```bash ```bash
# Validate myresume.json against either the FRESH or JSON Resume schema. # Validate myresume.json against either the FRESH or JSON Resume schema.
hackmyresume VALIDATE resumeA.json resumeB.json hackmyresume validate resumeA.json resumeB.json
``` ```
HackMyResume will validate each specified resume in turn: HackMyResume will validate each specified resume in turn:
@ -461,7 +467,7 @@ HackMyResume can convert between the [FRESH][fresca] and [JSON Resume][6]
formats. Just run: formats. Just run:
```bash ```bash
hackmyresume CONVERT <INPUTS> <OUTPUTS> hackmyresume convert <INPUTS> <OUTPUTS>
``` ```
where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and
@ -475,31 +481,34 @@ You can pass options into HackMyResume via an external options or ".hackmyrc"
file with the `--options` or `-o` switch: file with the `--options` or `-o` switch:
```bash ```bash
hackmyresume BUILD resume.json -o path/to/options.json hackmyresume build resume.json -o path/to/options.json
``` ```
The options file can contain any documented HackMyResume option, including The options file can contain any documented HackMyResume option, including
`theme`, `silent`, `debug`, `pdf`, `css`, and other settings. `theme`, `silent`, `debug`, `pdf`, `css`, and other settings.
```javascript ```json
{ {
// Set the default theme to "compact"
"theme": "compact", "theme": "compact",
// Change the "employment" section title text to "Work"
"sectionTitles": { "sectionTitles": {
"employment": "Work" "employment": "Work"
},
"wkhtmltopdf": {
"margin-top": "20mm"
} }
} }
``` ```
If a particular option is specified both on the command line and in an external If an option is specified on both the command line and in an external options
options file, the explicit command-line option takes precedence. file, the command-line option wins.
```bash ```bash
# path/to/options.json specifes the POSITIVE theme # path/to/options.json specifes the POSITIVE theme
# -t parameter specifies the COMPACT theme # -t parameter specifies the COMPACT theme
# The -t parameter wins. # The -t parameter wins.
hackmyresume BUILD resume.json -o path/to/options.json -t compact hackmyresume build resume.json -o path/to/options.json -t compact
> Reading resume: resume.json > Reading resume: resume.json
> Applying COMPACT theme (7 formats) > Applying COMPACT theme (7 formats)
``` ```
@ -511,7 +520,7 @@ HTML-formatted resumes. To disable prettification, the `--no-prettify` or `-n`
flag can be used: flag can be used:
```bash ```bash
hackmyresume BUILD resume.json out.all --no-prettify hackmyresume build resume.json out.all --no-prettify
``` ```
### Silent Mode ### Silent Mode
@ -519,8 +528,8 @@ hackmyresume BUILD resume.json out.all --no-prettify
Use `-s` or `--silent` to run in silent mode: Use `-s` or `--silent` to run in silent mode:
```bash ```bash
hackmyresume BUILD resume.json -o someFile.all -s hackmyresume build resume.json -o someFile.all -s
hackmyresume BUILD resume.json -o someFile.all --silent hackmyresume build resume.json -o someFile.all --silent
``` ```
### Debug Mode ### Debug Mode
@ -529,15 +538,80 @@ Use `-d` or `--debug` to force HMR to emit a call stack when errors occur. In
the future, this option will emit detailed error logging. the future, this option will emit detailed error logging.
```bash ```bash
hackmyresume BUILD resume.json -d hackmyresume build resume.json -d
hackmyresume ANALYZE resume.json --debug hackmyresume analyze resume.json --debug
``` ```
### Disable Encoding
Use the `--no-escape` option to disable encoding in Handlebars themes. Note:
this option has no effect for non-Handlebars themes.
```bash
hackmyresume build resume.json --no-escape
```
### Private Resume Fields
Have a gig, education stint, membership, or other relevant history that you'd
like to hide from *most* (e.g. public) resumes but sometimes show on others? Tag it with
`"private": true` to omit it from outbound generated resumes by default.
```json
"employment": {
"history": [
{
"employer": "Acme Real Estate"
},
{
"employer": "Area 51 Alien Research Laboratory",
"private": true
},
{
"employer": "H&R Block"
}
]
}
```
Then, when you want a copy of your resume that includes the private gig / stint
/ etc., tell HackMyResume that it's OK to emit private fields. The way you do
that is with the `--private` switch.
```bash
hackmyresume build resume.json private-resume.all --private
```
### Custom theme helpers
You can attach your own custom Handlebars helpers to a FRESH theme with the
`helpers` key of your theme's `theme.json` file.
```js
{
"title": "my-cool-theme",
// ...
"helpers": [
"../path/to/helpers/*.js",
"some-other-helper.js"
]
}
```
HackMyResume will attempt to load each path or glob and register any specified
files with [Handlebars.registerHelper][hrh], making them available to your
theme.
## Contributing ## Contributing
HackMyResume is a community-driven free and open source project under the MIT 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, License. Contributions are encouraged and we respond to all PRs and issues in
usually within 24 hours. See [CONTRIBUTING.md][contribute] for details. time. See [CONTRIBUTING.md][contribute] for details.
## License ## License
@ -553,8 +627,9 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
[8]: https://youtu.be/N9wsjroVlu8 [8]: https://youtu.be/N9wsjroVlu8
[9]: https://api.jquery.com/jquery.extend/ [9]: https://api.jquery.com/jquery.extend/
[10]: https://github.com/beautify-web/js-beautify [10]: https://github.com/beautify-web/js-beautify
[11]: http://weasyprint.org/
[fresh]: https://github.com/fluentdesk/FRESH [fresh]: https://github.com/fluentdesk/FRESH
[fresca]: https://github.com/fluentdesk/FRESCA [fresca]: https://github.com/fresh-standard/fresh-resume-schema
[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself [dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
[img-release]: https://img.shields.io/github/release/hacksalot/HackMyResume.svg?label=version [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-master]: https://img.shields.io/travis/hacksalot/HackMyResume/master.svg
@ -565,3 +640,6 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
[contribute]: CONTRIBUTING.md [contribute]: CONTRIBUTING.md
[fresh-themes]: https://github.com/fluentdesk/fresh-themes [fresh-themes]: https://github.com/fluentdesk/fresh-themes
[jrst]: https://www.npmjs.com/search?q=jsonresume-theme [jrst]: https://www.npmjs.com/search?q=jsonresume-theme
[gh]: https://gitter.im/hacksalot/HackMyResume?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
[badge]: https://badges.gitter.im/hacksalot/HackMyResume.svg
[hrh]: http://handlebarsjs.com/reference.html#base-registerHelper

107
ROADMAP.md Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

BIN
assets/hmr_analyze.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/hmr_build.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a476ee59e7d86b5a7599780b5efca57ee6b6d60e1a722343277057ea793703b6
size 1642116

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

2800
npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
{ {
"name": "hackmyresume", "name": "hackmyresume",
"version": "1.6.0", "version": "1.9.0-beta",
"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.", "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": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/hacksalot/HackMyResume.git" "url": "https://github.com/hacksalot/HackMyResume.git"
}, },
"scripts": { "scripts": {
"test": "grunt clean && mocha", "test": "grunt clean:test && mocha --exit",
"grunt": "grunt" "grunt": "grunt"
}, },
"keywords": [ "keywords": [
@ -31,6 +31,7 @@
"author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)", "author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
"contributors": [ "contributors": [
"aruberto (https://github.com/aruberto)", "aruberto (https://github.com/aruberto)",
"daniele-rapagnani (https://github.com/daniele-rapagnani)",
"jjanusch (https://github.com/driftdev)", "jjanusch (https://github.com/driftdev)",
"robertmain (https://github.com/robertmain)", "robertmain (https://github.com/robertmain)",
"tomheon (https://github.com/tomheon)", "tomheon (https://github.com/tomheon)",
@ -42,61 +43,67 @@
"bugs": { "bugs": {
"url": "https://github.com/hacksalot/HackMyResume/issues" "url": "https://github.com/hacksalot/HackMyResume/issues"
}, },
"main": "src/hackmyapi.js",
"bin": { "bin": {
"hackmyresume": "src/index.js" "hackmyresume": "src/cli/index.js"
}, },
"main": "src/index.js",
"homepage": "https://github.com/hacksalot/HackMyResume", "homepage": "https://github.com/hacksalot/HackMyResume",
"dependencies": { "dependencies": {
"chalk": "^1.1.1", "chalk": "^2.3.1",
"commander": "^2.9.0", "commander": "^2.9.0",
"copy": "^0.1.3", "copy": "^0.3.1",
"escape-latex": "^1.0.0",
"extend": "^3.0.0", "extend": "^3.0.0",
"fresca": "~0.6.0", "fresh-jrs-converter": "^1.0.0",
"fresh-jrs-converter": "^0.2.0", "fresh-resume-schema": "^1.0.0-beta",
"fresh-resume-starter": "^0.2.2", "fresh-resume-starter": "^0.3.1",
"fresh-themes": "~0.13.0-beta", "fresh-resume-validator": "^0.2.0",
"fs-extra": "^0.24.0", "fresh-themes": "^0.17.0-beta",
"fs-extra": "^5.0.0",
"glob": "^7.1.2",
"handlebars": "^4.0.5", "handlebars": "^4.0.5",
"html": "0.0.10", "html": "^1.0.0",
"is-my-json-valid": "^2.12.2", "is-my-json-valid": "^2.12.4",
"json-lint": "^0.1.0", "json-lint": "^0.1.0",
"jst": "0.0.13", "jsonlint": "^1.6.2",
"lodash": "^3.10.1", "lodash": "^4.17.5",
"marked": "^0.3.5", "marked": "^0.3.5",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"moment": "^2.10.6", "moment": "^2.11.1",
"parse-filepath": "^0.6.3", "parse-filepath": "^1.0.2",
"path-exists": "^2.1.0", "path-exists": "^3.0.0",
"pinkie-promise": "^2.0.0",
"printf": "^0.2.3", "printf": "^0.2.3",
"recursive-readdir-sync": "^1.0.6", "recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.2.0", "simple-html-tokenizer": "^0.4.3",
"slash": "^1.0.0", "slash": "^1.0.0",
"string-padding": "^1.0.2", "string-padding": "^1.0.2",
"string.prototype.endswith": "^0.2.0", "string.prototype.endswith": "^0.2.0",
"string.prototype.startswith": "^0.2.0", "string.prototype.startswith": "^0.2.0",
"traverse": "^0.6.6", "traverse": "^0.6.6",
"underscore": "^1.8.3", "underscore": "^1.8.3",
"webshot": "^0.16.0",
"word-wrap": "^1.1.0", "word-wrap": "^1.1.0",
"xml-escape": "^1.0.0", "xml-escape": "^1.0.0",
"yamljs": "^0.2.4" "yamljs": "^0.3.0"
}, },
"devDependencies": { "devDependencies": {
"chai": "*", "chai": "*",
"fresh-test-resumes": "^0.6.0", "chai-as-promised": "^7.1.1",
"dir-compare": "^1.4.0",
"fresh-test-resumes": "^0.9.2",
"fresh-test-themes": "^0.2.0",
"fresh-theme-underscore": "^0.1.1",
"grunt": "*", "grunt": "*",
"grunt-cli": "^0.1.13", "grunt-contrib-clean": "^1.1.0",
"grunt-contrib-clean": "^0.7.0", "grunt-contrib-coffee": "^2.0.0",
"grunt-contrib-jshint": "^0.11.3", "grunt-contrib-copy": "^1.0.0",
"grunt-contrib-yuidoc": "^0.10.0", "grunt-eslint": "^20.1.0",
"grunt-jsdoc": "^1.1.0",
"grunt-simple-mocha": "*", "grunt-simple-mocha": "*",
"jsonresume-theme-boilerplate": "^0.1.2", "jsonresume-theme-boilerplate": "^0.1.2",
"jsonresume-theme-classy": "^1.0.9", "jsonresume-theme-classy": "^1.0.9",
"jsonresume-theme-modern": "0.0.18", "jsonresume-theme-modern": "0.0.18",
"jsonresume-theme-sceptile": "^1.0.5", "jsonresume-theme-sceptile": "^1.0.5",
"mocha": "*", "mocha": "*",
"resample": "fluentdesk/resample" "stripcolorcodes": "^0.1.0"
} }
} }

View File

@ -1,3 +1,9 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Error-handling routines for HackMyResume. Error-handling routines for HackMyResume.
@module cli/error @module cli/error
@ -6,123 +12,108 @@ Error-handling routines for HackMyResume.
(function() { const HMSTATUS = require('../core/status-codes');
const FS = require('fs');
const PATH = require('path');
const WRAP = require('word-wrap');
var HMSTATUS = require('../core/status-codes') const M2C = require('../utils/md2chalk');
, PKG = require('../../package.json') const chalk = require('chalk');
, FS = require('fs') const extend = require('extend');
, FCMD = require('../hackmyapi') const printf = require('printf');
, PATH = require('path') const SyntaxErrorEx = require('../utils/syntax-error-ex');
, WRAP = require('word-wrap')
, M2C = require('../utils/md2chalk.js')
, chalk = require('chalk')
, extend = require('extend')
, YAML = require('yamljs')
, printf = require('printf')
, SyntaxErrorEx = require('../utils/syntax-error-ex');
require('string.prototype.startswith'); require('string.prototype.startswith');
/** /** Error handler for HackMyResume. All errors are handled here.
Error handler for HackMyResume. All errors are handled here. @class ErrorHandler */
@class ErrorHandler module.exports = {
*/
var ErrorHandler = module.exports = {
init( debug, assert, silent ) {
init: function( debug, assert, silent ) {
this.debug = debug; this.debug = debug;
this.assert = assert; this.assert = assert;
this.silent = silent; this.silent = silent;
this.msgs = require('./msg.js').errors; this.msgs = require('./msg').errors;
return this; return this;
}, },
err( ex, shouldExit ) {
err: function( ex, shouldExit ) {
// Short-circuit logging output if --silent is on // Short-circuit logging output if --silent is on
var o = this.silent ? function() { } : _defaultLog; let stack;
const o = this.silent ? function() {} : _defaultLog;
// Special case; can probably be removed. // Special case; can probably be removed.
if( ex.pass ) throw ex; if (ex.pass) { throw ex; }
// Load error messages // Load error messages
this.msgs = this.msgs || require('./msg.js').errors; this.msgs = this.msgs || require('./msg').errors;
// Handle packaged HMR exceptions // Handle packaged HMR exceptions
if (ex.fluenterror) { if (ex.fluenterror) {
// Output the error message // Output the error message
var objError = assembleError.call( this, ex ); const objError = assembleError.call(this, ex);
o( this[ 'format_' + objError.etype ]( objError.msg )); o( this[ `format_${objError.etype}` ]( objError.msg ));
// Output the stack (sometimes) // Output the stack (sometimes)
if (objError.withStack) { if (objError.withStack) {
var stack = ex.stack || (ex.inner && ex.inner.stack); stack = ex.stack || (ex.inner && ex.inner.stack);
stack && o( chalk.gray( stack ) ); stack && o( chalk.gray( stack ) );
} }
// Quit if necessary // Quit if necessary
if( ex.quit || objError.quit ) { if (shouldExit || ex.exit) {
this.debug && o( if (this.debug) {
chalk.cyan('Exiting with error code ' + ex.fluenterror.toString())); o(chalk.cyan(`Exiting with error code ${ex.fluenterror.toString()}`));
if( this.assert ) { ex.pass = true; throw ex; }
process.exit( ex.fluenterror );
} }
if (this.assert) {
ex.pass = true;
throw ex;
}
return process.exit(ex.fluenterror);
} }
// Handle raw exceptions // Handle raw exceptions
else { } else {
o(ex); o(ex);
var stackTrace = ex.stack || (ex.inner && ex.inner.stack); const stackTrace = ex.stack || (ex.inner && ex.inner.stack);
if( stackTrace && this.debug ) if (stackTrace && this.debug) {
o( M2C(ex.stack || ex.inner.stack, 'gray') ); return o(M2C(ex.stack || ex.inner.stack, 'gray'));
}
} }
}, },
format_error: function( msg ) { format_error( msg ) {
msg = msg || ''; msg = msg || '';
return chalk.red.bold( return chalk.red.bold( msg.toUpperCase().startsWith('ERROR:') ? msg : `Error: ${msg}` );
msg.toUpperCase().startsWith('ERROR:') ? msg : 'Error: ' + msg );
}, },
format_warning( brief, msg ) {
format_warning: function( brief, msg ) {
return chalk.yellow(brief) + chalk.yellow(msg || ''); return chalk.yellow(brief) + chalk.yellow(msg || '');
}, },
format_custom: function( msg ) { format_custom( msg ) { return msg; }
return msg;
}
}; };
var _defaultLog = function() { return console.log.apply(console.log, arguments); }; // eslint-disable-line no-console
function _defaultLog() {
console.log.apply( console.log, arguments );
}
function assembleError( ex ) {
var msg = '', withStack = false, quit = false, etype = 'warning'; var assembleError = function( ex ) {
if( this.debug ) withStack = true;
let se;
let msg = '';
let withStack = false;
let quit = false;
let etype = 'warning';
if (this.debug) { withStack = true; }
switch (ex.fluenterror) { switch (ex.fluenterror) {
@ -136,18 +127,20 @@ Error-handling routines for HackMyResume.
break; break;
case HMSTATUS.resumeNotFound: case HMSTATUS.resumeNotFound:
msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' ); //msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' );
msg += M2C(FS.readFileSync(
PATH.resolve(__dirname, `help/${ex.verb}.txt`), 'utf8' ), 'white', 'yellow');
break; break;
case HMSTATUS.missingCommand: case HMSTATUS.missingCommand:
msg = M2C( this.msgs.missingCommand.msg + " (", 'yellow'); // msg = M2C( this.msgs.missingCommand.msg + " (", 'yellow');
msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) { // msg += Object.keys( FCMD.verbs ).map( (v, idx, ar) ->
return (idx === ar.length - 1 ? chalk.yellow('or ') : '') + // return ( if idx == ar.length - 1 then chalk.yellow('or ') else '') +
chalk.yellow.bold(v.toUpperCase()); // chalk.yellow.bold(v.toUpperCase());
}).join( chalk.yellow(', ')) + chalk.yellow(").\n\n"); // ).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += chalk.gray(FS.readFileSync( msg += M2C(FS.readFileSync(
PATH.resolve(__dirname, '../cli/use.txt'), 'utf8' )); PATH.resolve(__dirname, 'help/use.txt'), 'utf8' ), 'white', 'yellow');
break; break;
case HMSTATUS.invalidCommand: case HMSTATUS.invalidCommand:
@ -168,8 +161,9 @@ Error-handling routines for HackMyResume.
case HMSTATUS.pdfGeneration: case HMSTATUS.pdfGeneration:
msg = M2C( this.msgs.pdfGeneration.msg, 'bold' ); msg = M2C( this.msgs.pdfGeneration.msg, 'bold' );
if( ex.inner ) msg += chalk.red('\n' + ex.inner); if (ex.inner) { msg += chalk.red(`\n${ex.inner}`); }
withStack = true; quit = false; etype = 'error'; quit = false;
etype = 'error';
break; break;
case HMSTATUS.invalid: case HMSTATUS.invalid:
@ -191,9 +185,10 @@ Error-handling routines for HackMyResume.
case HMSTATUS.invalidFormat: case HMSTATUS.invalidFormat:
ex.data.forEach( function(d) { ex.data.forEach( function(d) {
msg += printf( M2C( this.msgs.invalidFormat.msg, 'bold' ), return msg += printf( M2C( this.msgs.invalidFormat.msg, 'bold' ),
ex.theme.name.toUpperCase(), d.format.toUpperCase()); ex.theme.name.toUpperCase(), d.format.toUpperCase());
}, this); }
, this);
break; break;
case HMSTATUS.missingParam: case HMSTATUS.missingParam:
@ -203,9 +198,9 @@ Error-handling routines for HackMyResume.
case HMSTATUS.invalidHelperUse: case HMSTATUS.invalidHelperUse:
msg = printf( M2C( this.msgs.invalidHelperUse.msg ), ex.helper ); msg = printf( M2C( this.msgs.invalidHelperUse.msg ), ex.helper );
if (ex.error) { if (ex.error) {
msg += '\n--> ' + assembleError.call( this, extend( true, {}, ex, {fluenterror: ex.error} )).msg; msg += `\n--> ${assembleError.call( this, extend( true, {}, ex, {fluenterror: ex.error} )).msg}`;
//msg += printf( '\n--> ' + M2C( this.msgs.invalidParamCount.msg ), ex.expected );
} }
//msg += printf( '\n--> ' + M2C( this.msgs.invalidParamCount.msg ), ex.expected );
quit = false; quit = false;
etype = 'warning'; etype = 'warning';
break; break;
@ -217,8 +212,10 @@ Error-handling routines for HackMyResume.
break; break;
case HMSTATUS.readError: case HMSTATUS.readError:
if( !ex.quiet ) if (!ex.quiet) {
// eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file)); console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file));
}
msg = ex.inner.toString(); msg = ex.inner.toString();
etype = 'error'; etype = 'error';
break; break;
@ -230,7 +227,7 @@ Error-handling routines for HackMyResume.
case HMSTATUS.invokeTemplate: case HMSTATUS.invokeTemplate:
msg = M2C(this.msgs.invokeTemplate.msg, 'red'); msg = M2C(this.msgs.invokeTemplate.msg, 'red');
msg += M2C( '\n' + WRAP(ex.inner.toString(), { width: 60, indent: ' ' }), 'gray' ); msg += M2C( `\n${WRAP(ex.inner.toString(), { width: 60, indent: ' ' })}`, 'gray' );
etype = 'custom'; etype = 'custom';
break; break;
@ -249,32 +246,83 @@ Error-handling routines for HackMyResume.
case HMSTATUS.parseError: case HMSTATUS.parseError:
if (SyntaxErrorEx.is(ex.inner)) { if (SyntaxErrorEx.is(ex.inner)) {
// eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file )); console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file ));
var se = new SyntaxErrorEx( ex, ex.raw ); se = new SyntaxErrorEx(ex, ex.raw);
msg = printf( M2C( this.msgs.parseError.msg, 'red' ), if ((se.line != null) && (se.col != null)) {
se.line, se.col); msg = printf(M2C( this.msgs.parseError.msg[0], 'red' ), se.line, se.col);
} else if (se.line != null) {
msg = printf(M2C( this.msgs.parseError.msg[1], 'red' ), se.line);
} else {
msg = M2C(this.msgs.parseError.msg[2], 'red');
} }
else if( ex.inner && ex.inner.line !== undefined && ex.inner.col !== undefined ) { } else if (ex.inner && (ex.inner.line != null) && (ex.inner.col != null)) {
msg = printf( M2C( this.msgs.parseError.msg, 'red' ), msg = printf( M2C( this.msgs.parseError.msg[0], 'red' ), ex.inner.line, ex.inner.col);
ex.inner.line, ex.inner.col); } else {
}
else {
msg = ex; msg = ex;
} }
etype = 'error'; etype = 'error';
break; break;
case HMSTATUS.createError:
// inner.code could be EPERM, EACCES, etc
msg = printf(M2C( this.msgs.createError.msg ), ex.inner.path);
etype = 'error';
break;
case HMSTATUS.validateError:
msg = printf(M2C( this.msgs.validateError.msg ), ex.inner.toString());
etype = 'error';
break;
case HMSTATUS.invalidOptionsFile:
msg = M2C(this.msgs.invalidOptionsFile.msg[0]);
if (SyntaxErrorEx.is(ex.inner)) {
// eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file ));
se = new SyntaxErrorEx(ex, ex.raw);
if ((se.line != null) && (se.col != null)) {
msg += printf(M2C( this.msgs.parseError.msg[0], 'red' ), se.line, se.col);
} else if (se.line != null) {
msg += printf(M2C( this.msgs.parseError.msg[1], 'red' ), se.line);
} else {
msg += M2C(this.msgs.parseError.msg[2], 'red');
}
} else if (ex.inner && (ex.inner.line != null) && (ex.inner.col != null)) {
msg += printf( M2C( this.msgs.parseError.msg[0], 'red' ), ex.inner.line, ex.inner.col);
} else {
msg += ex;
}
msg += this.msgs.invalidOptionsFile.msg[1];
etype = 'error';
break;
case HMSTATUS.optionsFileNotFound:
msg = M2C( this.msgs.optionsFileNotFound.msg );
etype = 'error';
break;
case HMSTATUS.unknownSchema:
msg = M2C( this.msgs.unknownSchema.msg[0] );
//msg += "\n" + M2C( @msgs.unknownSchema.msg[1], 'yellow' )
etype = 'error';
break;
case HMSTATUS.themeHelperLoad:
msg = printf(M2C( this.msgs.themeHelperLoad.msg ), ex.glob);
etype = 'error';
break;
case HMSTATUS.invalidSchemaVersion:
msg = printf(M2C( this.msgs.invalidSchemaVersion.msg ), ex.data);
etype = 'error';
break;
} }
return { return {
msg: msg, // The error message to display msg, // The error message to display
withStack: withStack, // Whether to include the stack withStack, // Whether to include the stack
quit: quit, quit,
etype: etype etype
};
}; };
}
}());

25
src/cli/help/analyze.txt Normal file
View File

@ -0,0 +1,25 @@
**analyze** | Analyze a resume for statistical insight
Usage:
**hackmyresume ANALYZE <resume>**
The ANALYZE command evaluates the specified resume(s) for
coverage, duration, gaps, keywords, and other metrics.
This command can be run against multiple resumes. Each
will be analyzed in turn.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified, separated by spaces.
hackmyresume ANALYZE resume.json
hackmyresume ANALYZE r1.json r2.json r3.json
Options:
**None.**

69
src/cli/help/build.txt Normal file
View File

@ -0,0 +1,69 @@
**build** | Generate themed resumes in multiple formats
Usage:
**hackmyresume BUILD <resume> TO <target> [--theme]**
**[--pdf] [--no-escape] [--private]**
The BUILD command generates themed resumes and CVs in
multiple formats. Use it to create outbound resumes in
specific formats such HTML, MS Word, and PDF.
Parameters:
**<resume>**
Path to a FRESH or JRS resume (*.json) containing your
resume data. Multiple resumes may be specified.
If multiple resumes are specified, they will be merged
into a single resume prior to transformation.
**<target>**
Path to the desired output resume. Multiple resumes
may be specified. The file extension will determine
the format.
.all Generate all supported formats
.html HTML 5
.doc MS Word
.pdf Adobe Acrobat PDF
.txt plain text
.md Markdown
.png PNG Image
.latex LaTeX
Note: not all formats are supported by all themes!
Check the theme's documentation for details or use
the .all extension to build all available formats.
Options:
**--theme -t <theme-name-or-path>**
Path to a FRESH or JSON Resume theme OR the name of a
built-in theme. Valid theme names are 'modern',
'positive', 'compact', 'awesome', and 'basis'.
**--pdf -p <engine>**
Specify the PDF engine to use. Legal values are
'none', 'wkhtmltopdf', 'phantom', or 'weasyprint'.
**--no-escape**
Disable escaping / encoding of resume data during
resume generation. Handlebars themes only.
**--private**
Include resume fields marked as private.
Notes:
The BUILD command can be run against multiple source as well
as multiple target resumes. If multiple source resumes are
provided, they will be merged into a single source resume
before generation. If multiple output resumes are provided,
each will be generated in turn.

33
src/cli/help/convert.txt Normal file
View File

@ -0,0 +1,33 @@
**convert** | Convert resumes between FRESH and JRS formats
Usage:
**hackmyresume CONVERT <resume> TO <target> [--format]**
The CONVERT command converts one or more resume documents
between the FRESH Resume Schema and JSON Resume formats.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified.
**<targets>**
The path of the converted resume. Multiple resumes can
be specified, one per provided input resume.
Options:
**--format -f <fmt>**
The desired format for the new resume(s). Valid values
are 'FRESH', 'JRS', or, to target the latest edge
version of the JSON Resume Schema, 'JRS@1'.
If this parameter is omitted, the destination format
will be inferred from the source resume's format. If
the source format is FRESH, the destination format
will be JSON Resume, and vice-versa.

23
src/cli/help/help.txt Normal file
View File

@ -0,0 +1,23 @@
**help** | View help on a specific HackMyResume command
Usage:
**hackmyresume HELP [<command>]**
The HELP command displays help information for a specific
HackMyResume command, including the HELP command itself.
Parameters:
**<command>**
The HackMyResume command to view help information for.
Must be BUILD, NEW, CONVERT, ANALYZE, VALIDATE, PEEK,
or HELP.
hackmyresume help convert
hackmyresume help help
Options:
**None.**

29
src/cli/help/new.txt Normal file
View File

@ -0,0 +1,29 @@
**new** | Create a new FRESH or JRS resume document
Usage:
**hackmyresume NEW <fileName> [--format]**
The NEW command generates a new resume document in FRESH
or JSON Resume format. This document can serve as an
official source of truth for your resume and career data
as well an input to tools like HackMyResume.
Parameters:
**<fileName>**
The filename (relative or absolute path) of the resume
to be created. Multiple resume paths can be specified,
and each will be created in turn.
hackmyresume NEW resume.json
hackmyresume NEW r1.json foo/r2.json ../r3.json
Options:
**--format -f <fmt>**
The desired format for the new resume(s). Valid values
are 'FRESH', 'JRS', or, to target the latest edge
version of the JSON Resume Schema, 'JRS@1'.

31
src/cli/help/peek.txt Normal file
View File

@ -0,0 +1,31 @@
**peek** | View portions of a resume from the command line
Usage:
**hackmyresume PEEK <resume> <at>**
The PEEK command displays a specific piece or part of the
resume without requiring the resume to be opened in an
editor.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified, separated by spaces.
hackmyresume PEEK r1.json r2.json r3.json "employment.history[2]"
**<at>**
The resume property or field to be displayed. Can be
any valid resume path, for example:
education[0]
info.name
employment.history[3].start
Options:
**None.**

70
src/cli/help/use.txt Normal file
View File

@ -0,0 +1,70 @@
**HackMyResume** | A Swiss Army knife for resumes and CVs
Usage:
**hackmyresume [--version] [--help] [--silent] [--debug]**
**[--options] [--no-colors] <command> [<args>]**
Commands: (type "hackmyresume help COMMAND" for details)
**BUILD** Build your resume to the destination format(s).
**ANALYZE** Analyze your resume for keywords, gaps, and metrics.
**VALIDATE** Validate your resume for errors and typos.
**NEW** Create a new resume in FRESH or JSON Resume format.
**CONVERT** Convert your resume between FRESH and JSON Resume.
**PEEK** View a specific field or element on your resume.
**HELP** View help on a specific HackMyResume command.
Common Tasks:
Generate a resume in a specific format (HTML, Word, PDF, etc.)
**hackmyresume build rez.json to out/rez.html**
**hackmyresume build rez.json to out/rez.doc**
**hackmyresume build rez.json to out/rez.pdf**
**hackmyresume build rez.json to out/rez.txt**
**hackmyresume build rez.json to out/rez.md**
**hackmyresume build rez.json to out/rez.png**
**hackmyresume build rez.json to out/rez.tex**
Build a resume to ALL available formats:
**hackmyresume build rez.json to out/rez.all**
Build a resume with a specific theme:
**hackmyresume build rez.json to out/rez.all -t themeName**
Create a new empty resume:
**hackmyresume new rez.json**
Convert a resume between FRESH and JRS formats:
**hackmyresume convert rez.json converted.json**
Analyze a resume for important metrics
**hackmyresume analyze rez.json**
Find more resume themes:
**https://www.npmjs.com/search?q=jsonresume-theme**
**https://www.npmjs.com/search?q=fresh-theme**
**https://github.com/fresh-standard/fresh-themes**
Validate a resume's structure and syntax:
**hackmyresume validate resume.json**
View help on a specific command:
**hackmyresume help [build|convert|new|analyze|validate|peek|help]**
Submit a bug or request:
**https://githut.com/hacksalot/HackMyResume/issues**
HackMyResume is free and open source software published
under the MIT license. For more information, visit the
HackMyResume website or GitHub project page.

26
src/cli/help/validate.txt Normal file
View File

@ -0,0 +1,26 @@
**validate** | Validate a resume for correctness
Usage:
**hackmyresume VALIDATE <resume> [--assert]**
The VALIDATE command validates a FRESH or JRS document
against its governing schema, verifying that the resume
is correctly structured and formatted.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified.
hackmyresume ANALYZE resume.json
hackmyresume ANALYZE r1.json r2.json r3.json
Options:
**--assert -a**
Tell HackMyResume to return a non-zero process exit
code if a resume fails to validate.

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

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

View File

@ -1,3 +1,8 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the `main` function. Definition of the `main` function.
@module cli/main @module cli/main
@ -6,45 +11,47 @@ Definition of the `main` function.
(function(){ const HMR = require('../index');
const PKG = require('../../package.json');
const FS = require('fs');
const EXTEND = require('extend');
const chalk = require('chalk');
const PATH = require('path');
const HMSTATUS = require('../core/status-codes');
const safeLoadJSON = require('../utils/safe-json-loader');
//StringUtils = require '../utils/string.js'
const _ = require('underscore');
const OUTPUT = require('./out');
const PAD = require('string-padding');
const { Command } = require('commander');
const M2C = require('../utils/md2chalk');
const printf = require('printf');
const _opts = { };
const _title = chalk.white.bold(`\n*** HackMyResume v${PKG.version} ***`);
const _out = new OUTPUT( _opts );
const _err = require('./error');
let _exitCallback = null;
var HMR = require( '../hackmyapi') /*
, PKG = require('../../package.json')
, FS = require('fs')
, EXTEND = require('extend')
, chalk = require('chalk')
, PATH = require('path')
, HMSTATUS = require('../core/status-codes')
, HME = require('../core/event-codes')
, safeLoadJSON = require('../utils/safe-json-loader')
, StringUtils = require('../utils/string.js')
, _ = require('underscore')
, OUTPUT = require('./out')
, PAD = require('string-padding')
, Command = require('commander').Command;
var _opts = { };
var _title = chalk.white.bold('\n*** HackMyResume v' +PKG.version+ ' ***');
var _out = new OUTPUT( _opts );
/**
A callable implementation of the HackMyResume CLI. Encapsulates the command A callable implementation of the HackMyResume CLI. Encapsulates the command
line interface as a single method accepting a parameter array. line interface as a single method accepting a parameter array.
@alias module:cli/main.main @alias module:cli/main.main
@param rawArgs {Array} An array of command-line parameters. Will either be @param rawArgs {Array} An array of command-line parameters. Will either be
process.argv (in production) or custom parameters (in test). process.argv (in production) or custom parameters (in test).
*/ */
var main = module.exports = function( rawArgs ) { module.exports = function( rawArgs, exitCallback ) {
var initInfo = initialize( rawArgs ); const initInfo = initialize( rawArgs, exitCallback );
var args = initInfo.args; if (initInfo === null) {
return;
}
const { args } = initInfo;
// Create the top-level (application) command... // Create the top-level (application) command...
var program = new Command('hackmyresume') const program = new Command('hackmyresume')
.version(PKG.version) .version(PKG.version)
.description(chalk.yellow.bold('*** HackMyResume ***')) .description(chalk.yellow.bold('*** HackMyResume ***'))
.option('-s --silent', 'Run in silent mode') .option('-s --silent', 'Run in silent mode')
@ -63,9 +70,10 @@ Definition of the `main` function.
.option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH') .option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
.alias('create') .alias('create')
.description('Create resume(s) in FRESH or JSON RESUME format.') .description('Create resume(s) in FRESH or JSON RESUME format.')
.action(function( sources ) { .action((function( sources ) {
execute.call( this, sources, [], this.opts(), logMsg); execute.call( this, sources, [], this.opts(), logMsg);
}); })
);
// Create the VALIDATE command // Create the VALIDATE command
program program
@ -80,8 +88,9 @@ Definition of the `main` function.
program program
.command('convert') .command('convert')
.description('Convert a resume to/from FRESH or JSON RESUME format.') .description('Convert a resume to/from FRESH or JSON RESUME format.')
.option('-f --format <fmt>', 'FRESH or JRS format and optional version', undefined)
.action(function() { .action(function() {
var x = splitSrcDest.call( this ); const x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg); execute.call( this, x.src, x.dst, this.opts(), logMsg);
}); });
@ -89,6 +98,7 @@ Definition of the `main` function.
program program
.command('analyze') .command('analyze')
.arguments('<sources...>') .arguments('<sources...>')
.option('--private', 'Include resume fields marked as private', false)
.description('Analyze one or more resumes.') .description('Analyze one or more resumes.')
.action(function( sources ) { .action(function( sources ) {
execute.call( this, sources, [], this.opts(), logMsg); execute.call( this, sources, [], this.opts(), logMsg);
@ -99,8 +109,9 @@ Definition of the `main` function.
.command('peek') .command('peek')
.arguments('<sources...>') .arguments('<sources...>')
.description('Peek at a resume field or section') .description('Peek at a resume field or section')
.action(function( sources, sectionOrField ) { //.action(( sources, sectionOrField ) ->
var dst = (sources && sources.length > 1) ? [sources.pop()] : []; .action(function( sources ) {
const dst = (sources && (sources.length > 1)) ? [sources.pop()] : [];
execute.call( this, sources, dst, this.opts(), logMsg); execute.call( this, sources, dst, this.opts(), logMsg);
}); });
@ -114,25 +125,55 @@ Definition of the `main` function.
.option('-p --pdf <engine>', 'PDF generation engine') .option('-p --pdf <engine>', 'PDF generation engine')
.option('--no-sort', 'Sort resume sections by date', false) .option('--no-sort', 'Sort resume sections by date', false)
.option('--tips', 'Display theme tips and warnings.', false) .option('--tips', 'Display theme tips and warnings.', false)
.option('--private', 'Include resume fields marked as private', false)
.option('--no-escape', 'Turn off encoding in Handlebars themes.', false)
.description('Generate resume to multiple formats') .description('Generate resume to multiple formats')
.action(function( sources, targets, options ) { //.action(( sources, targets, options ) ->
var x = splitSrcDest.call( this ); .action(function() {
const x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg); execute.call( this, x.src, x.dst, this.opts(), logMsg);
}); });
// Create the HELP command
program
.command('help')
.arguments('[command]')
.description('Get help on a HackMyResume command')
.action(function( cmd ) {
cmd = cmd || 'use';
const manPage = FS.readFileSync(
PATH.join(__dirname, `help/${cmd}.txt`),
'utf8');
_out.log(M2C(manPage, 'white', 'yellow.bold'));
});
program.parse( args ); program.parse( args );
if (!program.args.length) { throw { fluenterror: 4 }; } if (!program.args.length) {
throw {fluenterror: 4};
}
}; };
/** Massage command-line args and setup Commander.js. */ /* Massage command-line args and setup Commander.js. */
function initialize( ar ) { var initialize = function( ar, exitCallback ) {
var o = initOptions( ar );
_exitCallback = exitCallback || process.exit;
const o = initOptions(ar);
if (o.ex) {
_err.init(false, true, false);
if( o.ex.op === 'parse' ) {
_err.err({
fluenterror: o.ex.op === 'parse' ? HMSTATUS.invalidOptionsFile : HMSTATUS.optionsFileNotFound,
inner: o.ex.inner,
quit: true
});
} else {
_err.err({fluenterror: HMSTATUS.optionsFileNotFound, inner: o.ex.inner, quit: true});
}
return null;
}
o.silent || logMsg( _title ); o.silent || logMsg( _title );
// Emit debug prelude if --debug was specified // Emit debug prelude if --debug was specified
@ -141,170 +182,222 @@ Definition of the `main` function.
_out.log(''); _out.log('');
_out.log(chalk.cyan(PAD(' Platform:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.platform === 'win32' ? 'windows' : process.platform )); _out.log(chalk.cyan(PAD(' Platform:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.platform === 'win32' ? 'windows' : process.platform ));
_out.log(chalk.cyan(PAD(' Node.js:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.version )); _out.log(chalk.cyan(PAD(' Node.js:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.version ));
_out.log(chalk.cyan(PAD(' HackMyResume:',25, null, PAD.RIGHT)) + chalk.cyan.bold('v' + PKG.version )); _out.log(chalk.cyan(PAD(' HackMyResume:',25, null, PAD.RIGHT)) + chalk.cyan.bold(`v${PKG.version}` ));
_out.log(chalk.cyan(PAD(' FRESCA:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies.fresca )); _out.log(chalk.cyan(PAD(' FRESCA:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies.fresca ));
_out.log(chalk.cyan(PAD(' fresh-themes:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-themes'] )); //_out.log(chalk.cyan(PAD(' fresh-themes:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-themes'] ))
_out.log(chalk.cyan(PAD(' fresh-jrs-converter:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-jrs-converter'] )); //_out.log(chalk.cyan(PAD(' fresh-jrs-converter:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-jrs-converter'] ))
_out.log(''); _out.log('');
} }
_err.init(o.debug, o.assert, o.silent);
// Handle invalid verbs here (a bit easier here than in commander.js)... // Handle invalid verbs here (a bit easier here than in commander.js)...
if( o.verb && !HMR.verbs[ o.verb ] && !HMR.alias[ o.verb ] ) { if (o.verb && !HMR.verbs[ o.verb ] && !HMR.alias[ o.verb ] && (o.verb !== 'help')) {
throw { fluenterror: HMSTATUS.invalidCommand, quit: true, _err.err({fluenterror: HMSTATUS.invalidCommand, quit: true, attempted: o.orgVerb}, true);
attempted: o.orgVerb };
} }
// Override the .missingArgument behavior // Override the .missingArgument behavior
Command.prototype.missingArgument = function(name) { Command.prototype.missingArgument = function() {
if( this.name() !== 'new' ) { if (this.name() !== 'help') {
throw { fluenterror: HMSTATUS.resumeNotFound, quit: true }; _err.err({
verb: this.name(),
fluenterror: HMSTATUS.resumeNotFound
}
, true);
} }
}; };
// Override the .helpInformation behavior // Override the .helpInformation behavior
Command.prototype.helpInformation = function() { Command.prototype.helpInformation = function() {
var manPage = FS.readFileSync( const manPage = FS.readFileSync(
PATH.join(__dirname, 'use.txt'), 'utf8' ); PATH.join(__dirname, 'help/use.txt'), 'utf8' );
return chalk.green.bold(manPage); return M2C(manPage, 'white', 'yellow');
}; };
return { return {
args: o.args, args: o.args,
options: o.json options: o.json
}; };
} };
/** Init options prior to setting up command infrastructure. */ /* Init options prior to setting up command infrastructure. */
function initOptions( ar ) { var initOptions = function( ar ) {
var oVerb, verb = '', args = ar.slice(), cleanArgs = args.slice(2), oJSON;
let oJSON, oVerb;
oVerb;
let verb = '';
const args = ar.slice();
const cleanArgs = args.slice( 2 );
oJSON;
if (cleanArgs.length) { if (cleanArgs.length) {
// Support case-insensitive sub-commands (build, generate, validate, etc) // Support case-insensitive sub-commands (build, generate, validate, etc)
var vidx = _.findIndex( cleanArgs, function(v){ return v[0] !== '-'; }); const vidx = _.findIndex(cleanArgs, v => v[0] !== '-');
if (vidx !== -1) { if (vidx !== -1) {
oVerb = cleanArgs[ vidx ]; oVerb = cleanArgs[ vidx ];
verb = args[ vidx + 2 ] = oVerb.trim().toLowerCase(); verb = (args[ vidx + 2 ] = oVerb.trim().toLowerCase());
} }
// Remove --options --opts -o and process separately // Remove --options --opts -o and process separately
var optsIdx = _.findIndex( cleanArgs, function(v){ const optsIdx = _.findIndex(cleanArgs, v => (v === '-o') || (v === '--options') || (v === '--opts'));
return v === '-o' || v === '--options' || v === '--opts';
});
if (optsIdx !== -1) { if (optsIdx !== -1) {
var optStr = cleanArgs[ optsIdx + 1]; let optStr = cleanArgs[ optsIdx + 1];
args.splice( optsIdx + 2, 2 ); args.splice( optsIdx + 2, 2 );
if (optStr && (optStr = optStr.trim())) { if (optStr && (optStr = optStr.trim())) {
//var myJSON = JSON.parse(optStr); //var myJSON = JSON.parse(optStr);
if( optStr[0] === '{') if( optStr[0] === '{') {
oJSON = eval('(' + optStr + ')'); // jshint ignore:line // TODO: remove use of evil(). - hacksalot
else { /* jshint ignore:start */
var inf = safeLoadJSON( optStr ); oJSON = eval(`(${optStr})`); // jshint ignore:line <-- no worky
if( !inf.ex ) /* jshint ignore:end */
} else {
const inf = safeLoadJSON( optStr );
if( !inf.ex ) {
oJSON = inf.json; oJSON = inf.json;
// TODO: Error handling } else {
return inf;
}
} }
} }
} }
} }
// Grab the --debug flag // Grab the --debug flag, --silent, --assert and --no-color flags
var isDebug = _.some( args, function(v) { const isDebug = _.some(args, v => (v === '-d') || (v === '--debug'));
return v === '-d' || v === '--debug'; const isSilent = _.some(args, v => (v === '-s') || (v === '--silent'));
}); const isAssert = _.some(args, v => (v === '-a') || (v === '--assert'));
const isMono = _.some(args, v => v === '--no-color');
// Grab the --silent flag const isNoEscape = _.some(args, v => v === '--no-escape');
var isSilent = _.some( args, function(v) {
return v === '-s' || v === '--silent';
});
return { return {
color: !isMono,
debug: isDebug, debug: isDebug,
silent: isSilent, silent: isSilent,
assert: isAssert,
noescape: isNoEscape,
orgVerb: oVerb, orgVerb: oVerb,
verb: verb, verb,
json: oJSON, json: oJSON,
args: args args
};
}; };
}
/** Invoke a HackMyResume verb. */ /* Invoke a HackMyResume verb. */
function execute( src, dst, opts, log ) { var execute = function( src, dst, opts, log ) {
// Create the verb
const v = new (HMR.verbs[ this.name() ])();
// Initialize command-specific options
loadOptions.call(this, opts, this.parent.jsonArgs); loadOptions.call(this, opts, this.parent.jsonArgs);
var hand = require( './error' );
hand.init( _opts.debug, _opts.assert, _opts.silent ); // Set up error/output handling
var v = new HMR.verbs[ this.name() ]();
_opts.errHandler = v; _opts.errHandler = v;
_out.init(_opts); _out.init(_opts);
v.on( 'hmr:status', function() { _out.do.apply( _out, arguments ); });
v.on( 'hmr:error', function() { // Hook up event notifications
hand.err.apply( hand, arguments ); v.on('hmr:status', function() { return _out.do.apply(_out, arguments); });
}); v.on('hmr:error', function() { return _err.err.apply(_err, arguments); });
v.invoke.call( v, src, dst, _opts, log );
if( v.errorCode ) // Invoke the verb using promise syntax
process.exit(v.errorCode); const prom = v.invoke.call(v, src, dst, _opts, log);
prom.then(executeSuccess, executeFail);
};
/* Success handler for verb invocations. Calls process.exit by default */
var executeSuccess = function() {};
// Can't call _exitCallback here (process.exit) when PDF is running in BK
//_exitCallback 0; return
/* Failure handler for verb invocations. Calls process.exit by default */
var executeFail = function(err) {
//console.dir err
let finalErrorCode = -1;
if (err) {
if (err.fluenterror) {
finalErrorCode = err.fluenterror;
} else if (err.length) {
finalErrorCode = err[0].fluenterror;
} else {
finalErrorCode = err;
} }
}
if (_opts.debug) {
const msgs = require('./msg').errors;
logMsg(printf(M2C( msgs.exiting.msg, 'cyan' ), finalErrorCode));
if (err.stack) { logMsg(err.stack); }
}
_exitCallback(finalErrorCode);
};
/** /*
Initialize HackMyResume options. Initialize HackMyResume options.
TODO: Options loading is a little hacky, for two reasons: TODO: Options loading is a little hacky, for two reasons:
- Commander.js idiosyncracies - Commander.js idiosyncracies
- Need to accept JSON inputs from the command line. - Need to accept JSON inputs from the command line.
*/ */
function loadOptions( o, cmdO ) { var loadOptions = function( o, cmdO ) {
// o and this.opts() seem to be the same (command-specific options) // o and this.opts() seem to be the same (command-specific options)
// Load the specified options file (if any) and apply options // Load the specified options file (if any) and apply options
if( cmdO ) if( cmdO ) {
o = EXTEND(true, o, cmdO); o = EXTEND(true, o, cmdO);
}
// Merge in command-line options // Merge in command-line options
o = EXTEND( true, o, this.opts() ); o = EXTEND( true, o, this.opts() );
// Kludge parent-level options until piping issue is resolved // Kludge parent-level options until piping issue is resolved
if( this.parent.silent !== undefined && this.parent.silent !== null) if ((this.parent.silent !== undefined) && (this.parent.silent !== null)) {
o.silent = this.parent.silent; o.silent = this.parent.silent;
if( this.parent.debug !== undefined && this.parent.debug !== null) }
if ((this.parent.debug !== undefined) && (this.parent.debug !== null)) {
o.debug = this.parent.debug; o.debug = this.parent.debug;
if( this.parent.assert !== undefined && this.parent.assert !== null) }
if ((this.parent.assert !== undefined) && (this.parent.assert !== null)) {
o.assert = this.parent.assert; o.assert = this.parent.assert;
}
if (o.debug) { if (o.debug) {
logMsg(chalk.cyan('OPTIONS:') + '\n'); logMsg(chalk.cyan('OPTIONS:') + '\n');
_.each(o, function(val, key) { _.each(o, (val, key) =>
logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'), logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'),
PAD(key,22,null,PAD.RIGHT), val); PAD(key,22,null,PAD.RIGHT), val)
}); );
logMsg(''); logMsg('');
} }
// Cache // Cache
EXTEND( true, _opts, o ); EXTEND( true, _opts, o );
};
/* Split multiple command-line filenames by the 'TO' keyword */
var splitSrcDest = function() {
const params = this.parent.args.filter(j => String.is(j));
if (params.length === 0) {
//tmpName = @name()
throw { fluenterror: HMSTATUS.resumeNotFound, verb: this.name(), quit: true };
} }
/** Split multiple command-line filenames by the 'TO' keyword */
function splitSrcDest() {
var params = this.parent.args.filter(function(j) { return String.is(j); });
if( params.length === 0 )
throw { fluenterror: HMSTATUS.resumeNotFound, quit: true };
// Find the TO keyword, if any // Find the TO keyword, if any
var splitAt = _.findIndex( params, function(p) { const splitAt = _.findIndex( params, p => p.toLowerCase() === 'to');
return p.toLowerCase() === 'to';
});
// TO can't be the last keyword // TO can't be the last keyword
if( splitAt === params.length - 1 && splitAt !== -1 ) { if ((splitAt === (params.length - 1)) && (splitAt !== -1)) {
logMsg(chalk.yellow('Please ') + logMsg(chalk.yellow('Please ') +
chalk.yellow.bold('specify an output file') + chalk.yellow.bold('specify an output file') +
chalk.yellow(' for this operation or ') + chalk.yellow(' for this operation or ') +
@ -317,15 +410,12 @@ Definition of the `main` function.
src: params.slice(0, splitAt === -1 ? undefined : splitAt ), src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
dst: splitAt === -1 ? [] : params.slice( splitAt + 1 ) dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
}; };
} };
/** Simple logging placeholder. */ /* Simple logging placeholder. */
function logMsg() { var logMsg = function() {
_opts.silent || console.log.apply( console.log, arguments ); // eslint-disable-next-line no-console
} return _opts.silent || console.log.apply( console.log, arguments );
};
}());

View File

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

View File

@ -3,6 +3,8 @@ events:
msg: Invoking **%s** command. msg: Invoking **%s** command.
beforeCreate: beforeCreate:
msg: Creating new **%s** resume: **%s** msg: Creating new **%s** resume: **%s**
afterCreate:
msg: Creating new **%s** resume: **%s**
afterRead: afterRead:
msg: Reading **%s** resume: **%s** msg: Reading **%s** resume: **%s**
beforeTheme: beforeTheme:
@ -41,6 +43,8 @@ events:
- "VALID!" - "VALID!"
- "INVALID" - "INVALID"
- "BROKEN" - "BROKEN"
- "MISSING"
- "ERROR"
beforePeek: beforePeek:
msg: msg:
- Peeking at **%s** in **%s** - Peeking at **%s** in **%s**
@ -79,7 +83,10 @@ errors:
readError: readError:
msg: Reading **???** resume: **%s** msg: Reading **???** resume: **%s**
parseError: parseError:
msg: Invalid or corrupt JSON on line %s column %s. msg:
- Invalid or corrupt JSON on line %s column %s.
- Invalid or corrupt JSON on line %s.
- Invalid or corrupt JSON.
invalidHelperUse: invalidHelperUse:
msg: "**Warning**: Incorrect use of the **%s** theme helper." msg: "**Warning**: Incorrect use of the **%s** theme helper."
fileSaveError: fileSaveError:
@ -96,3 +103,39 @@ errors:
msg: "Invalid number of parameters. Expected: **%s**." msg: "Invalid number of parameters. Expected: **%s**."
missingParam: missingParam:
msg: The '**%s**' parameter was needed but not supplied. msg: The '**%s**' parameter was needed but not supplied.
createError:
msg: Failed to create **'%s'**.
exiting:
msg: Exiting with status code **%s**.
validateError:
msg: "An error occurred during validation:\n%s"
invalidOptionsFile:
msg:
- "The specified options file is invalid:\n"
- "\nMake sure the options file contains valid JSON."
optionsFileNotFound:
msg: "The specified options file is missing or inaccessible."
unknownSchema:
msg:
- "Unknown resume schema. Did you specify a valid FRESH or JRS resume?"
- |
At a minimum, a FRESH resume must include a "name" field and a "meta"
property.
"name": "John Doe",
"meta": {
"format": "FRESH@0.1.0"
}
JRS-format resumes must include a "basics" section with a "name":
"basics": {
"name": "John Doe"
}
themeHelperLoad:
msg: >-
An error occurred while attempting to load the '%s' theme helper. Is the
theme correctly installed?
dummy: dontcare
invalidSchemaVersion:
msg: "'%s' is not recognized as a valid schema version."

View File

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

View File

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

View File

@ -1,11 +1,11 @@
(function(){ /*
Event code definitions.
/** @module core/default-formats
Supported resume formats. @license MIT. See LICENSE.md for details.
*/ */
/** Supported resume formats. */
module.exports = [ module.exports = [
{ name: 'html', ext: 'html', gen: new (require('../generators/html-generator'))() }, { name: 'html', ext: 'html', gen: new (require('../generators/html-generator'))() },
{ name: 'txt', ext: 'txt', gen: new (require('../generators/text-generator'))() }, { name: 'txt', ext: 'txt', gen: new (require('../generators/text-generator'))() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../generators/word-generator'))() }, { name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../generators/word-generator'))() },
@ -15,7 +15,4 @@
{ name: 'json', ext: 'json', gen: new (require('../generators/json-generator'))() }, { name: 'json', ext: 'json', gen: new (require('../generators/json-generator'))() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../generators/json-yaml-generator'))() }, { name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../generators/json-yaml-generator'))() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../generators/latex-generator'))() } { name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../generators/latex-generator'))() }
]; ];
}());

View File

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

View File

@ -1,17 +1,11 @@
/** /*
Event code definitions. Event code definitions.
@module event-codes.js @module core/event-codes
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
*/ */
(function(){
var val = 0;
module.exports = { module.exports = {
error: -1, error: -1,
success: 0, success: 0,
begin: 1, begin: 1,
@ -38,9 +32,8 @@ Event code definitions.
beforeInlineConvert: 22, beforeInlineConvert: 22,
afterInlineConvert: 23, afterInlineConvert: 23,
beforeValidate: 24, beforeValidate: 24,
afterValidate: 25 afterValidate: 25,
beforeWrite: 26,
afterWrite: 27,
applyTheme: 28
}; };
}());

View File

@ -1,12 +1,20 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
The HackMyResume date representation. The HackMyResume date representation.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@module core/fluent-date @module core/fluent-date
*/ */
(function(){
var moment = require('moment');
const moment = require('moment');
require('../utils/string');
/** /**
Create a FluentDate from a string or Moment date object. There are a few date Create a FluentDate from a string or Moment date object. There are a few date
@ -24,72 +32,64 @@ The HackMyResume date representation.
format or b) use an ISO format. For clarity, we handle these cases explicitly. format or b) use an ISO format. For clarity, we handle these cases explicitly.
@class FluentDate @class FluentDate
*/ */
function FluentDate( dt ) {
class FluentDate {
constructor(dt) {
this.rep = this.fmt(dt); this.rep = this.fmt(dt);
} }
FluentDate/*.prototype*/.fmt = function( dt, throws ) { static isCurrent(dt) {
return !dt || (String.is(dt) && /^(present|now|current)$/.test(dt));
}
}
throws = (throws === undefined || throws === null) || throws; const months = {};
const abbr = {};
moment.months().forEach((m,idx) => months[m.toLowerCase()] = idx+1);
moment.monthsShort().forEach((m,idx) => abbr[m.toLowerCase()]=idx+1);
abbr.sept = 9;
module.exports = FluentDate;
if( (typeof dt === 'string' || dt instanceof String) ) { FluentDate.fmt = function( dt, throws ) {
throws = ((throws === undefined) || (throws === null)) || throws;
if ((typeof dt === 'string') || dt instanceof String) {
dt = dt.toLowerCase().trim(); dt = dt.toLowerCase().trim();
if (/^(present|now|current)$/.test(dt)) { // "Present", "Now" if (/^(present|now|current)$/.test(dt)) { // "Present", "Now"
return moment(); return moment();
} } else if (/^\D+\s+\d{4}$/.test(dt)) { // "Mar 2015"
else if( /^\D+\s+\d{4}$/.test(dt) ) { // "Mar 2015" let left;
var parts = dt.split(' '); const parts = dt.split(' ');
var month = (months[parts[0]] || abbr[parts[0]]); const month = (months[parts[0]] || abbr[parts[0]]);
var temp = parts[1] + '-' + (month < 10 ? '0' + month : month.toString()); const temp = parts[1] + '-' + ((left = month < 10) != null ? left : `0${{month : month.toString()}}`);
return moment(temp, 'YYYY-MM'); return moment(temp, 'YYYY-MM');
} } else if (/^\d{4}-\d{1,2}$/.test(dt)) { // "2015-03", "1998-4"
else if( /^\d{4}-\d{1,2}$/.test(dt) ) { // "2015-03", "1998-4"
return moment(dt, 'YYYY-MM'); return moment(dt, 'YYYY-MM');
} } else if (/^\s*\d{4}\s*$/.test(dt)) { // "2015"
else if( /^\s*\d{4}\s*$/.test(dt) ) { // "2015"
return moment(dt, 'YYYY'); return moment(dt, 'YYYY');
} } else if (/^\s*$/.test(dt)) { // "", " "
else if( /^\s*$/.test(dt) ) { // "", " " return moment();
var defTime = { } else {
isNull: true, const mt = moment(dt);
isBefore: function( other ) { if (mt.isValid()) {
return( other && !other.isNull ) ? true : false;
},
isAfter: function( other ) {
return( other && !other.isNull ) ? false : false;
},
unix: function() { return 0; },
format: function() { return ''; },
diff: function() { return 0; }
};
return defTime;
}
else {
var mt = moment( dt );
if(mt.isValid())
return mt; return mt;
if( throws ) }
if (throws) {
throw 'Invalid date format encountered.'; throw 'Invalid date format encountered.';
}
return null; return null;
} }
} } else {
else {
if (!dt) { if (!dt) {
return moment(); return moment();
} } else if (dt.isValid && dt.isValid()) {
else if( dt.isValid && dt.isValid() )
return dt; return dt;
if( throws ) }
if (throws) {
throw 'Unknown date object encountered.'; throw 'Unknown date object encountered.';
}
return null; return null;
} }
}; };
var months = {}, abbr = {};
moment.months().forEach(function(m,idx){months[m.toLowerCase()]=idx+1;});
moment.monthsShort().forEach(function(m,idx){abbr[m.toLowerCase()]=idx+1;});
abbr.sept = 9;
module.exports = FluentDate;
}());

View File

@ -1,3 +1,9 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the FRESHResume class. Definition of the FRESHResume class.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@ -6,21 +12,15 @@ Definition of the FRESHResume class.
(function() { const FS = require('fs');
const extend = require('extend');
let validator = require('is-my-json-valid');
const _ = require('underscore');
var FS = require('fs') const __ = require('lodash');
, extend = require('extend') const XML = require('xml-escape');
, validator = require('is-my-json-valid') const MD = require('marked');
, _ = require('underscore') const CONVERTER = require('fresh-jrs-converter');
, __ = require('lodash') const JRSResume = require('./jrs-resume');
, PATH = require('path')
, moment = require('moment')
, XML = require('xml-escape')
, MD = require('marked')
, CONVERTER = require('fresh-jrs-converter')
, JRSResume = require('./jrs-resume');
@ -29,30 +29,15 @@ Definition of the FRESHResume class.
object is an instantiation of that JSON decorated with utility methods. object is an instantiation of that JSON decorated with utility methods.
@constructor @constructor
*/ */
function FreshResume() { class FreshResume {// extends AbstractResume
}
/** /** Initialize the the FreshResume from JSON string data. */
Initialize the FreshResume from file. parse( stringData, opts ) {
*/ this.imp = this.imp != null ? this.imp : {raw: stringData};
FreshResume.prototype.open = function( file, opts ) {
var raw = FS.readFileSync( file, 'utf8' );
var ret = this.parse( raw, opts );
this.imp.file = file;
return ret;
};
/**
Initialize the the FreshResume from JSON string data.
*/
FreshResume.prototype.parse = function( stringData, opts ) {
return this.parseJSON(JSON.parse( stringData ), opts); return this.parseJSON(JSON.parse( stringData ), opts);
}; }
@ -69,70 +54,83 @@ Definition of the FRESHResume class.
compute: Prepare computed resume totals. compute: Prepare computed resume totals.
} }
*/ */
FreshResume.prototype.parseJSON = function( rep, opts ) { parseJSON( rep, opts ) {
// Ignore any element with the 'ignore: true' designator. let scrubbed;
var that = this, traverse = require('traverse'), ignoreList = []; if (opts && opts.privatize) {
var scrubbed = traverse( rep ).map( function( x ) { // Ignore any element with the 'ignore: true' or 'private: true' designator.
if( !this.isLeaf && this.node.ignore ) { const scrubber = require('../utils/resume-scrubber');
if ( this.node.ignore === true || this.node.ignore === 'true' ) { var ret = scrubber.scrubResume(rep, opts);
ignoreList.push( this.node ); scrubbed = ret.scrubbed;
this.remove();
} }
}
});
// Now apply the resume representation onto this object // Now apply the resume representation onto this object
extend( true, this, scrubbed ); extend(true, this, opts && opts.privatize ? scrubbed : rep);
// If the resume already has a .imp object, then we are being called from // If the resume has already been processed, then we are being called from
// the .dupe method, and there's no need to do any post processing // the .dupe method, and there's no need to do any post processing
if( !this.imp ) { if (!(this.imp != null ? this.imp.processed : undefined)) {
// Set up metadata TODO: Clean up metadata on the object model. // Set up metadata TODO: Clean up metadata on the object model.
opts = opts || { }; opts = opts || { };
if( opts.imp === undefined || opts.imp ) { if ((opts.imp === undefined) || opts.imp) {
this.imp = this.imp || { }; this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.name; this.imp.title = (opts.title || this.imp.title) || this.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
} }
}
this.imp.processed = true;
// Parse dates, sort dates, and calculate computed values // Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this ); ((opts.date === undefined) || opts.date) && _parseDates.call( this );
(opts.sort === undefined || opts.sort) && this.sort(); ((opts.sort === undefined) || opts.sort) && this.sort();
(opts.compute === undefined || opts.compute) && (this.computed = { ((opts.compute === undefined) || opts.compute) && (this.computed = {
numYears: this.duration(), numYears: this.duration(),
keywords: this.keywords() keywords: this.keywords()
}); });
} }
return this; return this;
}; }
/** /** Save the sheet to disk (for environments that have disk access). */
Save the sheet to disk (for environments that have disk access). save( filename ) {
*/
FreshResume.prototype.save = function( filename ) {
this.imp.file = filename || this.imp.file; this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8'); FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
return this; return this;
}; }
/** /**
Save the sheet to disk in a specific format, either FRESH or JSON Resume. Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/ */
FreshResume.prototype.saveAs = function( filename, format ) { saveAs( filename, format ) {
if( format !== 'JRS' ) { // If format isn't specified, default to FRESH
const safeFormat = (format && format.trim()) || 'FRESH';
// Validate against the FRESH version regex
// freshVersionReg = require '../utils/fresh-version-regex'
// if (not freshVersionReg().test( safeFormat ))
// throw badVer: safeFormat
const parts = safeFormat.split('@');
if (parts[0] === 'FRESH') {
this.imp.file = filename || this.imp.file; this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8'); FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
}
else { } else if (parts[0] === 'JRS') {
var newRep = CONVERTER.toJRS( this ); const useEdgeSchema = parts.length > 1 ? parts[1] === '1' : false;
const newRep = CONVERTER.toJRS(this, {edge: useEdgeSchema});
FS.writeFileSync(filename, JRSResume.stringify( newRep ), 'utf8'); FS.writeFileSync(filename, JRSResume.stringify( newRep ), 'utf8');
} else {
throw {badVer: safeFormat};
} }
return this; return this;
}; }
@ -143,28 +141,12 @@ Definition of the FRESHResume class.
We do it this way to create a true clone of the object without re-running any We do it this way to create a true clone of the object without re-running any
of the associated processing. of the associated processing.
*/ */
FreshResume.prototype.dupe = function() { dupe() {
var jso = extend( true, { }, this ); const jso = extend(true, { }, this);
var rnew = new FreshResume(); const rnew = new FreshResume();
rnew.parseJSON(jso, { }); rnew.parseJSON(jso, { });
return rnew; 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 );
};
@ -172,9 +154,7 @@ Definition of the FRESHResume class.
Convert this object to a JSON string, sanitizing meta-properties along the Convert this object to a JSON string, sanitizing meta-properties along the
way. way.
*/ */
FreshResume.prototype.stringify = function() { stringify() { return FreshResume.stringify(this); }
return FreshResume.stringify( this );
};
@ -183,11 +163,11 @@ Definition of the FRESHResume class.
a transformation function (such as a Markdown filter or XML encoder). a transformation function (such as a Markdown filter or XML encoder).
TODO: Move this out of FRESHResume. TODO: Move this out of FRESHResume.
*/ */
FreshResume.prototype.transformStrings = function( filt, transformer ) { transformStrings( filt, transformer ) {
var ret = this.dupe(); const ret = this.dupe();
var trx = require('../utils/string-transformer'); const trx = require('../utils/string-transformer');
return trx(ret, filt, transformer); return trx(ret, filt, transformer);
}; }
@ -195,21 +175,19 @@ Definition of the FRESHResume class.
Create a copy of this resume in which all fields have been interpreted as Create a copy of this resume in which all fields have been interpreted as
Markdown. Markdown.
*/ */
FreshResume.prototype.markdownify = function() { markdownify() {
function MDIN( txt ){ const MDIN = txt => MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
}
function trx(key, val) { const trx = function( key, val ) {
if (key === 'summary') { if (key === 'summary') {
return MD(val); return MD(val);
} }
return MDIN(val); return MDIN(val);
} };
return this.transformStrings(['skills','url','start','end','date'], trx); return this.transformStrings(['skills','url','start','end','date'], trx);
}; }
@ -217,62 +195,64 @@ Definition of the FRESHResume class.
Create a copy of this resume in which all fields have been interpreted as Create a copy of this resume in which all fields have been interpreted as
Markdown. Markdown.
*/ */
FreshResume.prototype.xmlify = function() { xmlify() {
function trx(key, val) { const trx = (key, val) => XML(val);
return XML(val);
}
return this.transformStrings([], trx); return this.transformStrings([], trx);
}; }
/** /** Return the resume format. */
Return the resume format. format() { return 'FRESH'; }
*/
FreshResume.prototype.format = function() {
return 'FRESH';
};
/** /**
Return internal metadata. Create if it doesn't exist. Return internal metadata. Create if it doesn't exist.
*/ */
FreshResume.prototype.i = function() { i() { return this.imp = this.imp || { }; }
this.imp = (this.imp || { });
return this.imp;
};
/** /**
Return a unique list of all keywords across all skills. Return a unique list of all skills declared in the resume.
*/ */
FreshResume.prototype.keywords = function() {
var flatSkills = []; // TODO: Several problems here:
// 1) Confusing name. Easily confused with the keyword-inspector module, which
// parses resume body text looking for these same keywords. This should probably
// be renamed.
//
// 2) Doesn't bother trying to integrate skills.list with skills.sets if they
// happen to declare different skills, and if skills.sets declares ONE skill and
// skills.list declared 50, only 1 skill will be registered.
//
// 3) In the future, skill.sets should only be able to use skills declared in
// skills.list. That is, skills.list is the official record of a candidate's
// declared skills. skills.sets is just a way of grouping those into skillsets
// for easier consumption.
keywords() {
let flatSkills = [];
if (this.skills) { if (this.skills) {
if (this.skills.sets) { if (this.skills.sets) {
flatSkills = this.skills.sets.map(function(sk) { return sk.skills; }) flatSkills = this.skills.sets.map(sk => sk.skills).reduce( (a,b) => a.concat(b));
.reduce(function(a,b) { return a.concat(b); }); } else if (this.skills.list) {
} flatSkills = flatSkills.concat( this.skills.list.map(sk => sk.name) );
else if( this.skills.list ) {
flatSkills = flatSkills.concat( this.skills.list.map(function(sk) {
return sk.name;
}));
} }
flatSkills = _.uniq(flatSkills); flatSkills = _.uniq(flatSkills);
} }
return flatSkills; return flatSkills;
}, }
/** /**
Reset the sheet to an empty state. TODO: refactor/review Reset the sheet to an empty state. TODO: refactor/review
*/ */
FreshResume.prototype.clear = function( clearMeta ) { clear( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta; clearMeta = ((clearMeta === undefined) && true) || clearMeta;
clearMeta && (delete this.imp); if (clearMeta) { delete this.imp; }
delete this.computed; // Don't use Object.keys() here delete this.computed; // Don't use Object.keys() here
delete this.employment; delete this.employment;
delete this.service; delete this.service;
@ -282,75 +262,62 @@ Definition of the FRESHResume class.
delete this.writing; delete this.writing;
delete this.interests; delete this.interests;
delete this.skills; delete this.skills;
delete this.social; return delete this.social;
}; }
/** /**
Get a safe count of the number of things in a section. Get a safe count of the number of things in a section.
*/ */
FreshResume.prototype.count = function( obj ) { count( obj ) {
if( !obj ) return 0; if (!obj) { return 0; }
if( obj.history ) return obj.history.length; if (obj.history) { return obj.history.length; }
if( obj.sets ) return obj.sets.length; if (obj.sets) { return obj.sets.length; }
return obj.length || 0; return obj.length || 0;
}; }
/** /** Add work experience to the sheet. */
Get the default (starter) sheet. add( moniker ) {
*/ const defSheet = FreshResume.default();
FreshResume.default = function() { const newObject =
return new FreshResume().parseJSON( require('fresh-resume-starter') ); defSheet[moniker].history
}; ? extend( true, {}, defSheet[ moniker ].history[0] )
:
moniker === 'skills'
? extend( true, {}, defSheet.skills.sets[0] )
: extend( true, {}, defSheet[ moniker ][0] );
/**
Add work experience to the sheet.
*/
FreshResume.prototype.add = function( moniker ) {
var defSheet = FreshResume.default();
var newObject = defSheet[moniker].history ?
$.extend( true, {}, defSheet[ moniker ].history[0] ) :
(moniker === 'skills' ?
$.extend( true, {}, defSheet.skills.sets[0] ) :
$.extend( true, {}, defSheet[ moniker ][0] ));
this[ moniker ] = this[ moniker ] || []; this[ moniker ] = this[ moniker ] || [];
if( this[ moniker ].history ) if (this[ moniker ].history) {
this[ moniker ].history.push(newObject); this[ moniker ].history.push(newObject);
else if( moniker === 'skills' ) } else if (moniker === 'skills') {
this.skills.sets.push(newObject); this.skills.sets.push(newObject);
else } else {
this[ moniker ].push(newObject); this[ moniker ].push(newObject);
}
return newObject; return newObject;
}; }
/** /**
Determine if the sheet includes a specific social profile (eg, GitHub). Determine if the sheet includes a specific social profile (eg, GitHub).
*/ */
FreshResume.prototype.hasProfile = function( socialNetwork ) { hasProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase(); socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.some( this.social, function(p) { return this.social && _.some(this.social, p => p.network.trim().toLowerCase() === socialNetwork);
return p.network.trim().toLowerCase() === socialNetwork; }
});
};
/** /** Return the specified network profile. */
Return the specified network profile. getProfile( socialNetwork ) {
*/
FreshResume.prototype.getProfile = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase(); socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.find( this.social, function(sn) { return this.social && _.find(this.social, sn => sn.network.trim().toLowerCase() === socialNetwork);
return sn.network.trim().toLowerCase() === socialNetwork; }
});
};
@ -358,72 +325,45 @@ Definition of the FRESHResume class.
Return an array of profiles for the specified network, for when the user Return an array of profiles for the specified network, for when the user
has multiple eg. GitHub accounts. has multiple eg. GitHub accounts.
*/ */
FreshResume.prototype.getProfiles = function( socialNetwork ) { getProfiles( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase(); socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.filter( this.social, function(sn){ return this.social && _.filter(this.social, sn => sn.network.trim().toLowerCase() === socialNetwork);
return sn.network.trim().toLowerCase() === socialNetwork; }
});
};
/** /** Determine if the sheet includes a specific skill. */
Determine if the sheet includes a specific skill. hasSkill( skill ) {
*/
FreshResume.prototype.hasSkill = function( skill ) {
skill = skill.trim().toLowerCase(); skill = skill.trim().toLowerCase();
return this.skills && _.some( this.skills, function(sk) { return this.skills && _.some(this.skills, sk =>
return sk.keywords && _.some( sk.keywords, function(kw) { sk.keywords && _.some(sk.keywords, kw => kw.trim().toLowerCase() === skill)
return kw.trim().toLowerCase() === skill; );
}); }
});
};
/** /** Validate the sheet against the FRESH Resume schema. */
Validate the sheet against the FRESH Resume schema. isValid() {
*/ const schemaObj = require('fresh-resume-schema');
FreshResume.prototype.isValid = function( info ) { validator = require('is-my-json-valid');
var schemaObj = require('fresca'); const validate = validator( schemaObj, { // See Note [1].
var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // See Note [1].
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
}); });
var ret = validate( this ); const ret = validate(this);
if (!ret) { if (!ret) {
this.imp = this.imp || { }; this.imp = this.imp || { };
this.imp.validationErrors = validate.errors; this.imp.validationErrors = validate.errors;
} }
return ret; return ret;
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
FreshResume.prototype.duration = function(unit) {
unit = unit || 'years';
var empHist = __.get(this, 'employment.history');
if( empHist && empHist.length ) {
var firstJob = _.last( empHist );
var careerStart = firstJob.start ? firstJob.safe.start : '';
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
!careerStart.trim())
return 0;
var careerLast = _.max( empHist, function( w ) {
return( w.safe && w.safe.end ) ? w.safe.end.unix() : moment().unix();
});
return careerLast.safe.end.diff( careerStart, unit );
} }
return 0;
};
duration(unit) {
const inspector = require('../inspectors/duration-inspector');
return inspector.run(this, 'employment.history', 'start', 'end', unit);
}
@ -431,34 +371,56 @@ Definition of the FRESHResume class.
Sort dated things on the sheet by start date descending. Assumes that dates Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates(). on the sheet have been processed with _parseDates().
*/ */
FreshResume.prototype.sort = function( ) { sort() {
function byDateDesc(a,b) { const byDateDesc = function(a,b) {
return( a.safe.start.isBefore(b.safe.start) ) ? 1 if (a.safe.start.isBefore(b.safe.start)) {
: ( a.safe.start.isAfter(b.safe.start) && -1 ) || 0; return 1;
} } else { if (a.safe.start.isAfter(b.safe.start)) { return -1; } else { return 0; } }
};
function sortSection( key ) { const sortSection = function( key ) {
var ar = __.get(this, key); const ar = __.get(this, key);
if (ar && ar.length) { if (ar && ar.length) {
var datedThings = obj.filter( function(o) { return o.start; } ); const datedThings = ar.filter(o => o.start);
datedThings.sort( byDateDesc ); return datedThings.sort( byDateDesc );
}
} }
};
sortSection('employment.history'); sortSection('employment.history');
sortSection('education.history'); sortSection('education.history');
sortSection('service.history'); sortSection('service.history');
sortSection('projects'); sortSection('projects');
// this.awards && this.awards.sort( function(a, b) { return this.writing && this.writing.sort(function(a, b) {
// return( a.safeDate.isBefore(b.safeDate) ) ? 1 if (a.safe.date.isBefore(b.safe.date)) {
// : ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0; return 1;
// }); } else { return ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0; }
this.writing && this.writing.sort( function(a, b) {
return( a.safe.date.isBefore(b.safe.date) ) ? 1
: ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0;
}); });
}
}
/**
Get the default (starter) sheet.
*/
FreshResume.default = () => new FreshResume().parseJSON(require('fresh-resume-starter').fresh);
/**
Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
along the way.
*/
FreshResume.stringify = function( obj ) {
const replacer = function( key,value ) { // Exclude these keys from stringification
const exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'];
if (_.some( exKeys, val => key.trim() === val)) {
return undefined; } else { return value; }
};
return JSON.stringify(obj, replacer, 2);
}; };
@ -470,55 +432,48 @@ Definition of the FRESHResume class.
job.startDate is the date as entered by the user. job.safeStartDate is the job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing. parsed Moment.js date that we actually use in processing.
*/ */
function _parseDates() { var _parseDates = function() {
var _fmt = require('./fluent-date').fmt; const _fmt = require('./fluent-date').fmt;
var that = this; const that = this;
// TODO: refactor recursion // TODO: refactor recursion
function replaceDatesInObject( obj ) { var replaceDatesInObject = function( obj ) {
if( !obj ) return; if (!obj) { return; }
if (Object.prototype.toString.call( obj ) === '[object Array]') { if (Object.prototype.toString.call( obj ) === '[object Array]') {
obj.forEach(function(elem){ obj.forEach(elem => replaceDatesInObject( elem ));
replaceDatesInObject( elem );
});
}
else if (typeof obj === 'object') {
if( obj._isAMomentObject || obj.safe )
return; return;
Object.keys( obj ).forEach(function(key) { } else if (typeof obj === 'object') {
replaceDatesInObject( obj[key] ); if (obj._isAMomentObject || obj.safe) {
}); return;
}
Object.keys( obj ).forEach(key => replaceDatesInObject(obj[key]));
['start','end','date'].forEach(function(val) { ['start','end','date'].forEach(function(val) {
if ((obj[val] !== undefined) && (!obj.safe || !obj.safe[val])) { if ((obj[val] !== undefined) && (!obj.safe || !obj.safe[val])) {
obj.safe = obj.safe || { }; obj.safe = obj.safe || { };
obj.safe[ val ] = _fmt(obj[val]); obj.safe[ val ] = _fmt(obj[val]);
if (obj[val] && (val === 'start') && !obj.end) { if (obj[val] && (val === 'start') && !obj.end) {
obj.safe.end = _fmt('current'); obj.safe.end = _fmt('current');
return;
} }
} }
}); });
return;
} }
} };
Object.keys( this ).forEach(function(member) { Object.keys( this ).forEach(function(member) {
replaceDatesInObject(that[member]); replaceDatesInObject(that[member]);
}); });
};
}
/** /** Export the Sheet function/ctor. */
Export the Sheet function/ctor.
*/
module.exports = FreshResume; module.exports = FreshResume;
}());
// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats // Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
// in addition to YYYY-MM-DD. The original regex: // in addition to YYYY-MM-DD. The original regex:
// //

View File

@ -1,174 +1,226 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the FRESHTheme class. Definition of the FRESHTheme class.
@module fresh-theme.js @module core/fresh-theme
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
*/ */
(function() {
const FS = require('fs');
const _ = require('underscore');
const PATH = require('path');
const parsePath = require('parse-filepath');
const EXTEND = require('extend');
const HMSTATUS = require('./status-codes');
const loadSafeJson = require('../utils/safe-json-loader');
const READFILES = require('recursive-readdir-sync');
var FS = require('fs') /* A representation of a FRESH theme asset.
, validator = require('is-my-json-valid') @class FRESHTheme */
, _ = require('underscore') class FRESHTheme {
, PATH = require('path')
, parsePath = require('parse-filepath')
, pathExists = require('path-exists').sync
, EXTEND = require('extend')
, HMSTATUS = require('./status-codes')
, moment = require('moment')
, loadSafeJson = require('../utils/safe-json-loader')
, READFILES = require('recursive-readdir-sync');
/**
The FRESHTheme class is a representation of a FRESH theme
asset. See also: JRSTheme.
@class FRESHTheme
*/
function FRESHTheme() {
constructor() {
this.baseFolder = 'src';
} }
/* Open and parse the specified theme. */
open( themeFolder ) {
/**
Open and parse the specified theme.
*/
FRESHTheme.prototype.open = function( themeFolder ) {
this.folder = themeFolder; this.folder = themeFolder;
// Open the [theme-name].json file; should have the same name as folder
var pathInfo = parsePath( themeFolder );
// Set up a formats hash for the theme // Set up a formats hash for the theme
var formatsHash = { }; let formatsHash = { };
// Load the theme // Load the theme
var themeFile = PATH.join( themeFolder, 'theme.json' ); const themeFile = PATH.join(themeFolder, 'theme.json');
var themeInfo = loadSafeJson( themeFile ); const themeInfo = loadSafeJson(themeFile);
if( themeInfo.ex ) throw { if (themeInfo.ex) {
fluenterror: themeInfo.ex.operation === 'parse' ? throw{
HMSTATUS.parseError : HMSTATUS.readError, fluenterror:
themeInfo.ex.op === 'parse'
? HMSTATUS.parseError
: HMSTATUS.readError,
inner: themeInfo.ex.inner inner: themeInfo.ex.inner
}; };
}
var that = this;
// Move properties from the theme JSON file to the theme object // Move properties from the theme JSON file to the theme object
EXTEND(true, this, themeInfo.json); EXTEND(true, this, themeInfo.json);
// Check for an "inherits" entry in the theme JSON. // Check for an "inherits" entry in the theme JSON.
if (this.inherits) { if (this.inherits) {
var cached = { }; const cached = { };
_.each(this.inherits, function(th, key) { _.each(this.inherits, function(th, key) {
var themesFolder = require.resolve('fresh-themes'); // First, see if this is one of the predefined FRESH themes. There are
var d = parsePath( themeFolder ).dirname; // only a handful of these, but they may change over time, so we need to
var themePath = PATH.join(d, th); // query the official source of truth: the fresh-themes repository, which
// mounts the themes conveniently by name to the module object, and which
// is embedded locally inside the HackMyResume installation.
// TODO: merge this code with
let themePath;
const themesObj = require('fresh-themes');
if (_.has(themesObj.themes, th)) {
themePath = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/themes/',
th
);
} else {
const d = parsePath( th ).dirname;
themePath = PATH.join(d, th);
}
cached[ th ] = cached[th] || new FRESHTheme().open( themePath ); cached[ th ] = cached[th] || new FRESHTheme().open( themePath );
formatsHash[ key ] = cached[ th ].getFormat( key ); return formatsHash[ key ] = cached[ th ].getFormat( key );
}); });
} }
// Check for an explicit "formats" entry in the theme JSON. If it has one, // Load theme files
// then this theme declares its files explicitly. formatsHash = _load.call(this, formatsHash);
if( !!this.formats ) {
formatsHash = loadExplicit.call( this, formatsHash );
this.explicit = true;
}
else {
formatsHash = loadImplicit.call( this, formatsHash );
}
// Cache // Cache
this.formats = formatsHash; this.formats = formatsHash;
// Set the official theme name // Set the official theme name
this.name = parsePath( this.folder ).name; this.name = parsePath( this.folder ).name;
return this; return this;
}; }
/* Determine if the theme supports the specified output format. */
hasFormat( fmt ) { return _.has(this.formats, fmt); }
/* Determine if the theme supports the specified output format. */
getFormat( fmt ) { return this.formats[ fmt ]; }
}
/** /* Load and parse theme source files. */
Determine if the theme supports the specified output format. var _load = function(formatsHash) {
*/
FRESHTheme.prototype.hasFormat = function( fmt ) {
return _.has( this.formats, fmt );
};
const that = this;
const tplFolder = PATH.join(this.folder, this.baseFolder);
/**
Determine if the theme supports the specified output format.
*/
FRESHTheme.prototype.getFormat = function( fmt ) {
return this.formats[ fmt ];
};
/**
Load the theme implicitly, by scanning the theme folder for
files. TODO: Refactor duplicated code with loadExplicit.
*/
function loadImplicit(formatsHash) {
// Set up a hash of formats supported by this theme.
var that = this;
var major = false;
// Establish the base theme folder
var tplFolder = PATH.join( this.folder, 'src' );
// Iterate over all files in the theme folder, producing an array, fmts, // 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 // containing info for each file. While we're doing that, also build up
// the formatsHash object. // the formatsHash object.
var fmts = READFILES(tplFolder).map( function(absPath) { const fmts = READFILES(tplFolder).map(function(absPath) {
return _loadOne.call(this, absPath, formatsHash, tplFolder);
}
, this);
// Now, get all the CSS files...
this.cssFiles = fmts.filter(fmt => 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.
this.cssFiles.forEach(function(cssf) {
const idx = _.findIndex(fmts, fmt => fmt && (fmt.pre === cssf.pre) && (fmt.ext === 'html'));
cssf.major = false;
if (idx > -1) {
fmts[ idx ].css = cssf.data;
return fmts[ idx ].cssPath = cssf.path;
} else {
if (that.inherits) {
// Found a CSS file without an HTML file in a theme that inherits
// from another theme. This is the override CSS file.
return that.overrides = { file: cssf.path, data: cssf.data };
}
}});
// Now, save all the javascript file paths to a theme property.
const jsFiles = fmts.filter(fmt => fmt && (fmt.ext === 'js'));
this.jsFiles = jsFiles.map(jsf => jsf['path']);
return formatsHash;
};
/* Load a single theme file. */
var _loadOne = function( absPath, formatsHash, tplFolder ) {
const pathInfo = parsePath(absPath);
if (pathInfo.basename.toLowerCase() === 'theme.json') { return; }
const absPathSafe = absPath.trim().toLowerCase();
let outFmt = '';
let act = 'copy';
let isPrimary = false;
// If this is an "explicit" theme, all files of importance are specified in
// the "transform" section of the theme.json file.
if (this.explicit) {
outFmt = _.find(Object.keys( this.formats ), function( fmtKey ) {
const fmtVal = this.formats[ fmtKey ];
return _.some(fmtVal.transform, function(fpath) {
const absPathB = PATH.join( this.folder, fpath ).trim().toLowerCase();
return absPathB === absPathSafe;
}
, this);
}
, this);
if (outFmt) { act = 'transform'; }
}
if (!outFmt) {
// If this file lives in a specific format folder within the theme, // If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format // such as "/latex" or "/html", then that format is the implicit output
// for all files within the folder. // format for all files within the folder
var pathInfo = parsePath(absPath); const portion = pathInfo.dirname.replace(tplFolder,'');
var outFmt = '', isMajor = false;
var portion = pathInfo.dirname.replace(tplFolder,'');
if (portion && portion.trim()) { if (portion && portion.trim()) {
if( portion[1] === '_' ) return; if (portion[1] === '_') { return; }
var reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig; const reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
var res = reg.exec( portion ); const res = reg.exec( portion );
if (res) { if (res) {
if (res[1] !== 'partials') { if (res[1] !== 'partials') {
outFmt = res[1]; outFmt = res[1];
} if (!this.explicit) { act = 'transform'; }
else { } else {
that.partials = that.partials || []; this.partials = this.partials || [];
that.partials.push( { name: pathInfo.name, path: absPath } ); this.partials.push( { name: pathInfo.name, path: absPath } );
return null; return null;
} }
} }
} }
}
// Otherwise, the output format is inferred from the filename, as in // Otherwise, the output format is inferred from the filename, as in
// compact-[outputformat].[extension], for ex, compact-pdf.html. // compact-[outputformat].[extension], for ex, compact-pdf.html
if (!outFmt) { if (!outFmt) {
var idx = pathInfo.name.lastIndexOf('-'); const 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; if (!this.explicit) { act = 'transform'; }
const defFormats = require('./default-formats');
isPrimary = _.some(defFormats, form => (form.name === outFmt) && (pathInfo.extname !== '.css'));
} }
// We should have a valid output format now. // Make sure we have a valid formatsHash
formatsHash[ outFmt ] = formatsHash[outFmt] || { formatsHash[ outFmt ] = formatsHash[outFmt] || {
outFormat: outFmt, outFormat: outFmt,
files: [] files: []
}; };
// Create the file representation object. // Move symlink descriptions from theme.json to the format
var obj = { if (__guard__(this.formats != null ? this.formats[outFmt ] : undefined, x => x.symLinks)) {
action: 'transform', formatsHash[ outFmt ].symLinks = this.formats[ outFmt ].symLinks;
}
// Create the file representation object
const obj = {
action: act,
primary: isPrimary,
path: absPath, path: absPath,
major: isMajor,
orgPath: PATH.relative(tplFolder, absPath), orgPath: PATH.relative(tplFolder, absPath),
ext: pathInfo.extname.slice(1), ext: pathInfo.extname.slice(1),
title: friendlyName(outFmt), title: friendlyName(outFmt),
@ -181,158 +233,21 @@ Definition of the FRESHTheme class.
// Add this file to the list of files for this format type. // Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj ); formatsHash[ outFmt ].files.push( obj );
return obj; return obj;
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){
return fmt && (fmt.ext === 'css');
}))
// For each CSS file, get its corresponding HTML file. It's possible that
// a theme can have a CSS file but *no* HTML file, as when a theme author
// creates a pure CSS override of an existing theme.
.forEach(function( cssf ) {
var idx = _.findIndex(fmts, function( fmt ) {
return fmt && fmt.pre === cssf.pre && fmt.ext === 'html';
});
cssf.major = false;
if( idx > -1) {
fmts[ idx ].css = cssf.data;
fmts[ idx ].cssPath = cssf.path;
}
else {
if( that.inherits ) {
// Found a CSS file without an HTML file in a theme that inherits
// from another theme. This is the override CSS file.
that.overrides = { file: cssf.path, data: cssf.data };
}
}
});
return formatsHash;
}
/**
Load the theme explicitly, by following the 'formats' hash
in the theme's JSON settings file.
*/
function loadExplicit(formatsHash) {
// Housekeeping
var tplFolder = PATH.join( this.folder, 'src' );
var act = null;
var that = this;
// Iterate over all files in the theme folder, producing an array, fmts,
// containing info for each file. While we're doing that, also build up
// the formatsHash object.
var fmts = READFILES( tplFolder ).map( function( absPath ) {
act = null;
// If this file is mentioned in the theme's JSON file under "transforms"
var pathInfo = parsePath(absPath);
var absPathSafe = absPath.trim().toLowerCase();
var outFmt = _.find(
Object.keys( that.formats ),
function( fmtKey ) {
var fmtVal = that.formats[ fmtKey ];
return _.some( fmtVal.transform, function(fpath) {
var absPathB = PATH.join( that.folder, fpath )
.trim().toLowerCase();
return absPathB === absPathSafe;
});
});
if( outFmt ) {
act = 'transform';
}
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the output format
// for all files within the folder.
if( !outFmt ) {
var portion = pathInfo.dirname.replace(tplFolder,'');
if( portion && portion.trim() ) {
var reg = /^(?:\/|\\)(html|latex|doc|pdf)(?:\/|\\)?/ig;
var res = reg.exec( portion );
res && (outFmt = res[1]);
}
}
// Otherwise, the output format is inferred from the filename, as in
// compact-[outputformat].[extension], for ex, compact-pdf.html.
if( !outFmt ) {
var idx = pathInfo.name.lastIndexOf('-');
outFmt = (idx === -1) ? pathInfo.name : pathInfo.name.substr(idx + 1);
}
// We should have a valid output format now.
formatsHash[ outFmt ] =
formatsHash[ outFmt ] || {
outFormat: outFmt,
files: [],
symLinks: that.formats[ outFmt ].symLinks
}; };
// Create the file representation object.
var obj = {
action: act,
orgPath: PATH.relative(that.folder, absPath),
path: absPath,
ext: pathInfo.extname.slice(1),
title: friendlyName( outFmt ),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
data: FS.readFileSync( absPath, 'utf8' ),
css: null
};
// Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj );
return obj;
});
// Now, get all the CSS files...
(this.cssFiles = fmts.filter(function( fmt ){
return fmt.ext === 'css';
}))
// For each CSS file, get its corresponding HTML file
.forEach(function( cssf ) {
// For each CSS file, get its corresponding HTML file
var idx = _.findIndex(fmts, function( fmt ) {
return fmt.pre === cssf.pre && fmt.ext === 'html';
});
fmts[ idx ].css = cssf.data;
fmts[ idx ].cssPath = cssf.path;
});
// Remove CSS files from the formats array
fmts = fmts.filter( function( fmt) {
return fmt.ext !== 'css';
});
return formatsHash;
}
/* Return a more friendly name for certain formats. */
/** var friendlyName = function( val ) {
Return a more friendly name for certain formats. val = (val && val.trim().toLowerCase()) || '';
TODO: Refactor const friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
*/
function friendlyName( val ) {
val = val.trim().toLowerCase();
var friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val; return friendly[val] || val;
} };
module.exports = FRESHTheme; module.exports = FRESHTheme;
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}()); }

View File

@ -1,3 +1,10 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the JRSResume class. Definition of the JRSResume class.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@ -6,19 +13,12 @@ Definition of the JRSResume class.
(function() { const FS = require('fs');
const extend = require('extend');
let validator = require('is-my-json-valid');
const _ = require('underscore');
var FS = require('fs') const PATH = require('path');
, extend = require('extend') const CONVERTER = require('fresh-jrs-converter');
, validator = require('is-my-json-valid')
, _ = require('underscore')
, PATH = require('path')
, MD = require('marked')
, CONVERTER = require('fresh-jrs-converter')
, moment = require('moment');
/** /**
@ -26,36 +26,35 @@ Definition of the JRSResume class.
is an instantiation of that JSON decorated with utility methods. is an instantiation of that JSON decorated with utility methods.
@class JRSResume @class JRSResume
*/ */
function JRSResume() {
class JRSResume {
static initClass() {
/** Reset the sheet to an empty state. */
// clear = function( clearMeta ) {
// clearMeta = ((clearMeta === undefined) && true) || clearMeta;
// if (clearMeta) { delete this.imp; }
// delete this.basics.computed; // Don't use Object.keys() here
// delete this.work;
// delete this.volunteer;
// delete this.education;
// delete this.awards;
// delete this.publications;
// delete this.interests;
// delete this.skills;
// return delete this.basics.profiles;
// };
// extends AbstractResume
} }
/** /** Initialize the the JSResume from string. */
Initialize the JSResume from file. parse( stringData, opts ) {
*/ this.imp = this.imp != null ? this.imp : {raw: stringData};
JRSResume.prototype.open = function( file, title ) { return this.parseJSON(JSON.parse( stringData ), opts);
//this.imp = { fileName: file }; <-- schema violation, tuck it into .basics
this.basics = {
imp: {
file: file,
raw: FS.readFileSync( file, 'utf8' )
} }
};
return this.parse( this.basics.imp.raw, title );
};
/**
Initialize the the JSResume from string.
*/
JRSResume.prototype.parse = function( stringData, opts ) {
opts = opts || { };
var rep = JSON.parse( stringData );
return this.parseJSON( rep, opts );
};
@ -72,117 +71,87 @@ Definition of the JRSResume class.
compute: Prepare computed resume totals. compute: Prepare computed resume totals.
} }
*/ */
JRSResume.prototype.parseJSON = function( rep, opts ) { parseJSON( rep, opts ) {
let scrubbed;
opts = opts || { }; opts = opts || { };
if (opts.privatize) {
// Ignore any element with the 'ignore: true' designator. const scrubber = require('../utils/resume-scrubber');
var that = this, traverse = require('traverse'), ignoreList = []; // Ignore any element with the 'ignore: true' or 'private: true' designator.
var scrubbed = traverse( rep ).map( function( x ) { var ret = scrubber.scrubResume(rep, opts);
if( !this.isLeaf && this.node.ignore ) { scrubbed = ret.scrubbed;
if ( this.node.ignore === true || this.node.ignore === 'true' ) {
ignoreList.push( this.node );
this.remove();
} }
}
});
// Extend resume properties onto ourself. // Extend resume properties onto ourself.
extend( true, this, scrubbed ); extend(true, this, opts.privatize ? scrubbed : rep);
// Set up metadata // Set up metadata
if( opts.imp === undefined || opts.imp ) { if (!(this.imp != null ? this.imp.processed : undefined)) {
this.basics.imp = this.basics.imp || { }; // Set up metadata TODO: Clean up metadata on the object model.
this.basics.imp.title = opts = opts || { };
(opts.title || this.basics.imp.title) || this.basics.name; if ((opts.imp === undefined) || opts.imp) {
this.basics.imp.orgFormat = 'JRS'; this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.basics.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
}
}
this.imp.processed = true;
} }
// Parse dates, sort dates, and calculate computed values // Parse dates, sort dates, and calculate computed values
(opts.date === undefined || opts.date) && _parseDates.call( this ); ((opts.date === undefined) || opts.date) && _parseDates.call( this );
(opts.sort === undefined || opts.sort) && this.sort(); ((opts.sort === undefined) || opts.sort) && this.sort();
(opts.compute === undefined || opts.compute) && (this.basics.computed = { if ((opts.compute === undefined) || opts.compute) {
this.basics.computed = {
numYears: this.duration(), numYears: this.duration(),
keywords: this.keywords() keywords: this.keywords()
});
return this;
}; };
/**
Save the sheet to disk (for environments that have disk access).
*/
JRSResume.prototype.save = function( filename ) {
this.basics.imp.file = filename || this.basics.imp.file;
FS.writeFileSync(this.basics.imp.file, this.stringify( this ), 'utf8');
return this;
};
/**
Save the sheet to disk in a specific format, either FRESH or JRS.
*/
JRSResume.prototype.saveAs = function( filename, format ) {
if( format === 'JRS' ) {
this.basics.imp.file = filename || this.basics.imp.file;
FS.writeFileSync( this.basics.imp.file, this.stringify(), 'utf8' );
} }
else { return this;
var newRep = CONVERTER.toFRESH( this ); }
var stringRep = CONVERTER.toSTRING( newRep );
/** Save the sheet to disk (for environments that have disk access). */
save( filename ) {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify( this ), 'utf8');
return this;
}
/** Save the sheet to disk in a specific format, either FRESH or JRS. */
saveAs( filename, format ) {
if (format === 'JRS') {
this.imp.file = filename || this.imp.file;
FS.writeFileSync( this.imp.file, this.stringify(), 'utf8' );
} else {
const newRep = CONVERTER.toFRESH(this);
const stringRep = CONVERTER.toSTRING(newRep);
FS.writeFileSync(filename, stringRep, 'utf8'); FS.writeFileSync(filename, stringRep, 'utf8');
} }
return this; 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 the resume format. */
return JRSResume.stringify( this ); format() { return 'JRS'; }
};
/** stringify() { return JRSResume.stringify( this ); }
Return a unique list of all keywords across all skills.
*/
JRSResume.prototype.keywords = function() {
var flatSkills = []; /** Return a unique list of all keywords across all skills. */
keywords() {
let flatSkills = [];
if (this.skills && this.skills.length) { if (this.skills && this.skills.length) {
this.skills.forEach( function( s ) { this.skills.forEach( s => flatSkills = _.union(flatSkills, s.keywords));
flatSkills = _.union( flatSkills, s.keywords );
});
} }
return flatSkills; return flatSkills;
}; }
@ -191,125 +160,66 @@ Definition of the JRSResume class.
JSON Resume v0.0.0 doesn't allow additional properties at the root level, JSON Resume v0.0.0 doesn't allow additional properties at the root level,
so tuck this into the .basic sub-object. so tuck this into the .basic sub-object.
*/ */
JRSResume.prototype.i = function() { i() {
this.basics = this.basics || { }; return this.imp = this.imp != null ? this.imp : { };
this.basics.imp = this.basics.imp || { }; }
return this.basics.imp;
};
/** /** Add work experience to the sheet. */
Reset the sheet to an empty state. add( moniker ) {
*/ const defSheet = JRSResume.default();
JRSResume.prototype.clear = function( clearMeta ) { const newObject = extend( true, {}, defSheet[ moniker ][0] );
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
clearMeta && (delete this.imp);
delete this.basics.computed; // Don't use Object.keys() here
delete this.work;
delete this.volunteer;
delete this.education;
delete this.awards;
delete this.publications;
delete this.interests;
delete this.skills;
delete this.basics.profiles;
};
/**
Get the default (empty) sheet.
*/
JRSResume.default = function() {
return new JRSResume().open(
PATH.join( __dirname, 'empty-jrs.json'), 'Empty'
);
};
/**
Add work experience to the sheet.
*/
JRSResume.prototype.add = function( moniker ) {
var defSheet = JRSResume.default();
var newObject = $.extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || []; this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject ); this[ moniker ].push( newObject );
return newObject; return newObject;
}; }
/** /** Determine if the sheet includes a specific social profile (eg, GitHub). */
Determine if the sheet includes a specific social profile (eg, GitHub). hasProfile( socialNetwork ) {
*/
JRSResume.prototype.hasProfile = function( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase(); socialNetwork = socialNetwork.trim().toLowerCase();
return this.basics.profiles && _.some( this.basics.profiles, function(p) { return this.basics.profiles && _.some(this.basics.profiles, p => p.network.trim().toLowerCase() === socialNetwork);
return p.network.trim().toLowerCase() === socialNetwork; }
});
};
/** /** Determine if the sheet includes a specific skill. */
Determine if the sheet includes a specific skill. hasSkill( skill ) {
*/
JRSResume.prototype.hasSkill = function( skill ) {
skill = skill.trim().toLowerCase(); skill = skill.trim().toLowerCase();
return this.skills && _.some( this.skills, function(sk) { return this.skills && _.some(this.skills, sk =>
return sk.keywords && _.some( sk.keywords, function(kw) { sk.keywords && _.some(sk.keywords, kw => kw.trim().toLowerCase() === skill)
return kw.trim().toLowerCase() === skill; );
}); }
});
};
/** /** Validate the sheet against the JSON Resume schema. */
Validate the sheet against the JSON Resume schema. isValid( ) { // TODO: ↓ fix this path ↓
*/ const schema = FS.readFileSync(PATH.join( __dirname, 'resume.json' ), 'utf8');
JRSResume.prototype.isValid = function( ) { // TODO: ↓ fix this path ↓ const schemaObj = JSON.parse(schema);
var schema = FS.readFileSync( PATH.join( __dirname, 'resume.json' ),'utf8'); validator = require('is-my-json-valid');
var schemaObj = JSON.parse( schema ); const validate = validator( schemaObj, { // Note [1]
var validator = require('is-my-json-valid');
var validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ } formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
}); });
var ret = validate( this ); const temp = this.imp;
delete this.imp;
const ret = validate(this);
this.imp = temp;
if (!ret) { if (!ret) {
this.basics.imp = this.basics.imp || { }; this.imp = this.imp || { };
this.basics.imp.validationErrors = validate.errors; this.imp.validationErrors = validate.errors;
} }
return ret; return ret;
};
/**
Calculate the total duration of the sheet. Assumes this.work has been sorted
by start date descending, perhaps via a call to Sheet.sort().
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
JRSResume.prototype.duration = function( unit ) {
unit = unit || 'years';
if( this.work && this.work.length ) {
var careerStart = this.work[ this.work.length - 1].safeStartDate;
if ((typeof careerStart === 'string' || careerStart instanceof String) &&
!careerStart.trim())
return 0;
var careerLast = _.max( this.work, function( w ) {
return w.safeEndDate.unix();
}).safeEndDate;
return careerLast.diff( careerStart, unit );
} }
return 0;
};
duration(unit) {
const inspector = require('../inspectors/duration-inspector');
return inspector.run(this, 'work', 'startDate', 'endDate', unit);
}
@ -317,35 +227,38 @@ Definition of the JRSResume class.
Sort dated things on the sheet by start date descending. Assumes that dates Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates(). on the sheet have been processed with _parseDates().
*/ */
JRSResume.prototype.sort = function( ) { sort( ) {
const byDateDesc = function(a,b) {
if (a.safeStartDate.isBefore(b.safeStartDate)) {
return 1;
} else { return ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0; }
};
this.work && this.work.sort(byDateDesc); this.work && this.work.sort(byDateDesc);
this.education && this.education.sort(byDateDesc); this.education && this.education.sort(byDateDesc);
this.volunteer && this.volunteer.sort(byDateDesc); this.volunteer && this.volunteer.sort(byDateDesc);
this.awards && this.awards.sort(function(a, b) { this.awards && this.awards.sort(function(a, b) {
return( a.safeDate.isBefore(b.safeDate) ) ? 1 if (a.safeDate.isBefore(b.safeDate)) {
: ( a.safeDate.isAfter(b.safeDate) && -1 ) || 0; return 1;
}); } else { return (a.safeDate.isAfter(b.safeDate) && -1 ) || 0; }
this.publications && this.publications.sort( function(a, b) {
return( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) ? 1
: ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0;
}); });
function byDateDesc(a,b) { return this.publications && this.publications.sort(function(a, b) {
return( a.safeStartDate.isBefore(b.safeStartDate) ) ? 1 if ( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) {
: ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0; return 1;
} else { return ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0; }
});
} }
};
dupe() {
JRSResume.prototype.dupe = function() { const rnew = new JRSResume();
var rnew = new JRSResume();
rnew.parse(this.stringify(), { }); rnew.parse(this.stringify(), { });
return rnew; return rnew;
}; }
@ -353,60 +266,49 @@ Definition of the JRSResume class.
Create a copy of this resume in which all fields have been interpreted as Create a copy of this resume in which all fields have been interpreted as
Markdown. Markdown.
*/ */
JRSResume.prototype.harden = function() { harden() {
var that = this; const ret = this.dupe();
var ret = this.dupe();
function HD(txt) { const HD = txt => `@@@@~${txt}~@@@@`;
return '@@@@~' + txt + '~@@@@';
// const HDIN = txt =>
// //return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
// HD(txt)
// ;
const transformer = require('../utils/string-transformer');
return transformer(ret,
[ 'skills','url','website','startDate','endDate', 'releaseDate', 'date',
'phone','email','address','postalCode','city','country','region',
'safeStartDate','safeEndDate' ],
(key, val) => HD(val));
}
} }
function HDIN(txt){ JRSResume.initClass();
//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){ /** Get the default (empty) sheet. */
hardenStringsInObject( ret[ member ] ); JRSResume.default = () => new JRSResume().parseJSON(require('fresh-resume-starter').jrs);
});
return ret;
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
JRSResume.stringify = function( obj ) {
const replacer = function( key,value ) { // Exclude these keys from stringification
const temp = _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview', 'display_progress_bar'],
val => key.trim() === val);
if (temp) { return undefined; } else { return value; }
};
return JSON.stringify(obj, replacer, 2);
}; };
@ -418,37 +320,29 @@ Definition of the JRSResume class.
job.startDate is the date as entered by the user. job.safeStartDate is the job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing. parsed Moment.js date that we actually use in processing.
*/ */
function _parseDates() { var _parseDates = function() {
var _fmt = require('./fluent-date').fmt; const _fmt = require('./fluent-date').fmt;
this.work && this.work.forEach(function(job) { this.work && this.work.forEach(function(job) {
job.safeStartDate = _fmt( job.startDate ); job.safeStartDate = _fmt( job.startDate );
job.safeEndDate = _fmt( job.endDate ); return job.safeEndDate = _fmt( job.endDate );
}); });
this.education && this.education.forEach(function(edu) { this.education && this.education.forEach(function(edu) {
edu.safeStartDate = _fmt( edu.startDate ); edu.safeStartDate = _fmt( edu.startDate );
edu.safeEndDate = _fmt( edu.endDate ); return edu.safeEndDate = _fmt( edu.endDate );
}); });
this.volunteer && this.volunteer.forEach(function(vol) { this.volunteer && this.volunteer.forEach(function(vol) {
vol.safeStartDate = _fmt( vol.startDate ); vol.safeStartDate = _fmt( vol.startDate );
vol.safeEndDate = _fmt( vol.endDate ); return vol.safeEndDate = _fmt( vol.endDate );
}); });
this.awards && this.awards.forEach( function(awd) { this.awards && this.awards.forEach(awd => awd.safeDate = _fmt( awd.date ));
awd.safeDate = _fmt( awd.date ); return this.publications && this.publications.forEach(pub => pub.safeReleaseDate = _fmt( pub.releaseDate ));
}); };
this.publications && this.publications.forEach( function(pub) {
pub.safeReleaseDate = _fmt( pub.releaseDate );
});
}
/** /**
Export the JRSResume function/ctor. Export the JRSResume class.
*/ */
module.exports = JRSResume; module.exports = JRSResume;
}());

View File

@ -1,17 +1,20 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the JRSTheme class. Definition of the JRSTheme class.
@module jrs-theme.js @module core/jrs-theme
@license MIT. See LICENSE.MD for details. @license MIT. See LICENSE.MD for details.
*/ */
(function() {
const _ = require('underscore');
var _ = require('underscore') const PATH = require('path');
, PATH = require('path') const pathExists = require('path-exists').sync;
, parsePath = require('parse-filepath') const errors = require('./status-codes');
, pathExists = require('path-exists').sync;
@ -19,31 +22,24 @@ Definition of the JRSTheme class.
The JRSTheme class is a representation of a JSON Resume theme asset. The JRSTheme class is a representation of a JSON Resume theme asset.
@class JRSTheme @class JRSTheme
*/ */
function JRSTheme() { class JRSTheme {
}
/** /**
Open and parse the specified theme. Open and parse the specified JRS theme.
@method open @method open
*/ */
JRSTheme.prototype.open = function( thFolder ) { open( thFolder ) {
this.folder = thFolder; this.folder = thFolder;
//const pathInfo = parsePath(thFolder);
// Open the [theme-name].json file; should have the same // Open and parse the theme's package.json file
// name as folder const pkgJsonPath = PATH.join(thFolder, 'package.json');
var pathInfo = parsePath( thFolder );
// Open and parse the theme's package.json file.
var pkgJsonPath = PATH.join( thFolder, 'package.json' );
if (pathExists(pkgJsonPath)) { if (pathExists(pkgJsonPath)) {
const thApi = require(thFolder); // Requiring the folder yields whatever the package.json's "main" is set to
var thApi = require( thFolder ) const thPkg = require(pkgJsonPath); // Get the package.json as JSON
, thPkg = require( pkgJsonPath );
this.name = thPkg.name; this.name = thPkg.name;
this.render = (thApi && thApi.render) || undefined; this.render = (thApi && thApi.render) || undefined;
this.engine = 'jrs'; this.engine = 'jrs';
@ -52,32 +48,32 @@ Definition of the JRSTheme class.
// properties necessary to allow JSON Resume themes to share a rendering // properties necessary to allow JSON Resume themes to share a rendering
// path with FRESH themes. // path with FRESH themes.
this.formats = { this.formats = {
html: { outFormat: 'html', files: [ html: {
{ outFormat: 'html',
files: [{
action: 'transform', action: 'transform',
render: this.render, render: this.render,
major: true, primary: true,
ext: 'html', ext: 'html',
css: null css: null
} }]
]}, },
pdf: { outFormat: 'pdf', files: [ pdf: {
{ outFormat: 'pdf',
files: [{
action: 'transform', action: 'transform',
render: this.render, render: this.render,
major: true, primary: true,
ext: 'pdf', ext: 'pdf',
css: null css: null
}]
} }
]}
}; };
} else {
throw {fluenterror: errors.missingPackageJSON};
} }
else {
throw { fluenterror: HACKMYSTATUS.missingPackageJSON };
}
return this; return this;
}; }
@ -85,9 +81,7 @@ Definition of the JRSTheme class.
Determine if the theme supports the output format. Determine if the theme supports the output format.
@method hasFormat @method hasFormat
*/ */
JRSTheme.prototype.hasFormat = function( fmt ) { hasFormat( fmt ) { return _.has(this.formats, fmt); }
return _.has( this.formats, fmt );
};
@ -95,14 +89,8 @@ Definition of the JRSTheme class.
Return the requested output format. Return the requested output format.
@method getFormat @method getFormat
*/ */
JRSTheme.prototype.getFormat = function( fmt ) { getFormat( fmt ) { return this.formats[ fmt ]; }
return this.formats[ fmt ]; }
};
module.exports = JRSTheme; module.exports = JRSTheme;
}());

View File

@ -1,22 +1,21 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the ResumeFactory class. Definition of the ResumeFactory class.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@module resume-factory.js @module core/resume-factory
*/ */
(function(){ const FS = require('fs');
const HMS = require('./status-codes');
const HME = require('./event-codes');
const ResumeConverter = require('fresh-jrs-converter');
var FS = require('fs'), const resumeDetect = require('../utils/resume-detector');
HACKMYSTATUS = require('./status-codes'),
HME = require('./event-codes'),
ResumeConverter = require('fresh-jrs-converter'),
chalk = require('chalk'),
SyntaxErrorEx = require('../utils/syntax-error-ex'),
_ = require('underscore');
require('string.prototype.startswith'); require('string.prototype.startswith');
@ -25,7 +24,8 @@ Definition of the ResumeFactory class.
A simple factory class for FRESH and JSON Resumes. A simple factory class for FRESH and JSON Resumes.
@class ResumeFactory @class ResumeFactory
*/ */
var ResumeFactory = module.exports = {
module.exports = {
@ -44,47 +44,45 @@ Definition of the ResumeFactory class.
} }
*/ */
load: function ( sources, opts, emitter ) { load( sources, opts, emitter ) {
return sources.map( function(src) { return sources.map( function(src) {
return this.loadOne( src, opts, emitter ); return this.loadOne( src, opts, emitter );
}, this); }
, this);
}, },
/** Load a single resume from disk. */
loadOne( src, opts, emitter ) {
/** let toFormat = opts.format; // Can be null
Load a single resume from disk.
*/
loadOne: function( src, opts, emitter ) {
var toFormat = opts.format; // Can be null
var objectify = opts.objectify;
// Get the destination format. Can be 'fresh', 'jrs', or null/undefined. // Get the destination format. Can be 'fresh', 'jrs', or null/undefined.
toFormat && (toFormat = toFormat.toLowerCase().trim()); toFormat && (toFormat = toFormat.toLowerCase().trim());
// Load and parse the resume JSON // Load and parse the resume JSON
var info = _parse( src, opts, emitter ); const info = _parse(src, opts, emitter);
if( info.fluenterror ) return info; if (info.fluenterror) { return info; }
// Determine the resume format: FRESH or JRS // Determine the resume format: FRESH or JRS
var json = info.json; let { json } = info;
var orgFormat = ( json.meta && json.meta.format && const orgFormat = resumeDetect(json);
json.meta.format.startsWith('FRESH@') ) ? if (orgFormat === 'unk') {
'fresh' : 'jrs'; info.fluenterror = HMS.unknownSchema;
return info;
}
// Convert between formats if necessary // Convert between formats if necessary
if (toFormat && ( orgFormat !== toFormat )) { if (toFormat && ( orgFormat !== toFormat )) {
json = ResumeConverter[ 'to' + toFormat.toUpperCase() ]( json ); json = ResumeConverter[ `to${toFormat.toUpperCase()}` ](json);
} }
// Objectify the resume, that is, convert it from JSON to a FRESHResume // Objectify the resume, that is, convert it from JSON to a FRESHResume
// or JRSResume object. // or JRSResume object.
var rez; let rez = null;
if( objectify ) { if (opts.objectify) {
var ResumeClass = require('../core/' + (toFormat || orgFormat) + '-resume'); const reqLib = `../core/${toFormat || orgFormat}-resume`;
const ResumeClass = require(reqLib);
rez = new ResumeClass().parseJSON( json, opts.inner ); rez = new ResumeClass().parseJSON( json, opts.inner );
rez.i().file = src; rez.i().file = src;
} }
@ -92,16 +90,15 @@ Definition of the ResumeFactory class.
return { return {
file: src, file: src,
json: info.json, json: info.json,
rez: rez rez
}; };
} }
}; };
var _parse = function( fileName, opts, eve ) {
function _parse( fileName, opts, eve ) { let rawData = null;
var rawData;
try { try {
// Read the file // Read the file
@ -111,28 +108,20 @@ Definition of the ResumeFactory class.
// Parse the file // Parse the file
eve && eve.stat(HME.beforeParse, { data: rawData }); eve && eve.stat(HME.beforeParse, { data: rawData });
var ret = { json: JSON.parse( rawData ) }; const ret = { json: JSON.parse( rawData ) };
var orgFormat = ( ret.json.meta && ret.json.meta.format && const orgFormat =
ret.json.meta.format.startsWith('FRESH@') ) ? ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@')
'fresh' : 'jrs'; ? 'fresh' : 'jrs';
eve && eve.stat(HME.afterParse, { file: fileName, data: ret.json, fmt: orgFormat }); eve && eve.stat(HME.afterParse, { file: fileName, data: ret.json, fmt: orgFormat });
return ret; return ret;
} } catch (err) {
catch( e ) {
// Can be ENOENT, EACCES, SyntaxError, etc. // Can be ENOENT, EACCES, SyntaxError, etc.
var ex = { return {
fluenterror: rawData ? HACKMYSTATUS.parseError : HACKMYSTATUS.readError, fluenterror: rawData ? HMS.parseError : HMS.readError,
inner: e, raw: rawData, file: fileName, shouldExit: false inner: err,
raw: rawData,
file: fileName
}; };
opts.quit && (ex.quit = true);
eve && eve.err( ex.fluenterror, ex );
if( opts.throw ) throw ex;
return ex;
} }
};
}
}());

View File

@ -1,10 +1,9 @@
/** /**
Status codes for HackMyResume. Status codes for HackMyResume.
@module status-codes.js @module core/status-codes
@license MIT. See LICENSE.MD for details. @license MIT. See LICENSE.MD for details.
*/ */
(function(){
module.exports = { module.exports = {
success: 0, success: 0,
@ -16,7 +15,7 @@ Status codes for HackMyResume.
resumeNotFoundAlt: 6, resumeNotFoundAlt: 6,
inputOutputParity: 7, inputOutputParity: 7,
createNameMissing: 8, createNameMissing: 8,
pdfgeneration: 9, pdfGeneration: 9,
missingPackageJSON: 10, missingPackageJSON: 10,
invalid: 11, invalid: 11,
invalidFormat: 12, invalidFormat: 12,
@ -31,7 +30,12 @@ Status codes for HackMyResume.
compileTemplate: 21, compileTemplate: 21,
themeLoad: 22, themeLoad: 22,
invalidParamCount: 23, invalidParamCount: 23,
missingParam: 24 missingParam: 24,
createError: 25,
validateError: 26,
invalidOptionsFile: 27,
optionsFileNotFound: 28,
unknownSchema: 29,
themeHelperLoad: 30,
invalidSchemaVersion: 31
}; };
}());

View File

@ -1,39 +1,37 @@
/*
* decaffeinate suggestions:
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the BaseGenerator class. Definition of the BaseGenerator class.
@module base-generator.js @module generators/base-generator
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
*/ */
(function() {
// Use J. Resig's nifty class implementation
var Class = require( '../utils/class' );
/** /**
The BaseGenerator class is the root of the generator hierarchy. Functionality The BaseGenerator class is the root of the generator hierarchy. Functionality
common to ALL generators lives here. common to ALL generators lives here.
*/ */
var BaseGenerator = module.exports = Class.extend({ let BaseGenerator;
module.exports = (BaseGenerator = (function() {
BaseGenerator = class BaseGenerator {
static initClass() {
/** /** Status codes. */
Base-class initialize. this.prototype.codes = require('../core/status-codes');
*/
init: function( outputFormat ) {
this.format = outputFormat;
},
/**
Status codes.
*/
codes: require('../core/status-codes'),
/**
Generator options.
*/
opts: {
/** Generator options. */
this.prototype.opts = { };
} }
}); /** Base-class initialize. */
}()); constructor( format ) {
this.format = format;
}
};
BaseGenerator.initClass();
return BaseGenerator;
})());

View File

@ -1,34 +1,39 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the HTMLGenerator class. Definition of the HTMLGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module generators/html-generator
@module html-generator.js @license MIT. See LICENSE.md for details.
*/ */
(function() {
var TemplateGenerator = require('./template-generator')
, FS = require('fs-extra') const TemplateGenerator = require('./template-generator');
, HTML = require( 'html' ) const HTML = require('html');
, PATH = require('path');
require('string.prototype.endswith'); require('string.prototype.endswith');
var HtmlGenerator = module.exports = TemplateGenerator.extend({
init: function() {
this._super( 'html' ); class HtmlGenerator extends TemplateGenerator {
},
constructor() { super('html'); }
/** /**
Copy satellite CSS files to the destination and optionally pretty-print Copy satellite CSS files to the destination and optionally pretty-print
the HTML resume prior to saving. the HTML resume prior to saving.
*/ */
onBeforeSave: function( info ) { onBeforeSave( info ) {
if( info.outputFile.endsWith('.css') ) if (info.outputFile.endsWith('.css')) {
return info.mk; return info.mk;
return this.opts.prettify ? }
HTML.prettyPrint( info.mk, this.opts.prettify ) : info.mk; if (this.opts.prettify) {
return HTML.prettyPrint(info.mk, this.opts.prettify);
} else { return info.mk; }
}
} }
});
}()); module.exports = HtmlGenerator;

View File

@ -1,22 +1,23 @@
/*
* decaffeinate suggestions:
* DS103: Rewrite code to no longer use __guard__
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the HtmlPdfCLIGenerator class. Definition of the HtmlPdfCLIGenerator class.
@module html-pdf-generator.js @module generators/html-pdf-generator.js
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
*/ */
(function() { const TemplateGenerator = require('./template-generator');
const FS = require('fs-extra');
const PATH = require('path');
const SLASH = require('slash');
var TemplateGenerator = require('./template-generator') const _ = require('underscore');
, FS = require('fs-extra') const HMSTATUS = require('../core/status-codes');
, HTML = require( 'html' ) const SPAWN = require('../utils/safe-spawn');
, PATH = require('path')
, SPAWN = require('../utils/safe-spawn')
, SLASH = require('slash');
/** /**
@ -24,42 +25,40 @@ Definition of the HtmlPdfCLIGenerator class.
wkhtmltopdf, and other PDF engines over a CLI (command-line interface). wkhtmltopdf, and other PDF engines over a CLI (command-line interface).
If an engine isn't installed for a particular platform, error out gracefully. If an engine isn't installed for a particular platform, error out gracefully.
*/ */
var HtmlPdfCLIGenerator = module.exports = TemplateGenerator.extend({
class HtmlPdfCLIGenerator extends TemplateGenerator {
init: function() { constructor() { super('pdf', 'html'); }
this._super( 'pdf', 'html' );
},
/** /** Generate the binary PDF. */
Generate the binary PDF. onBeforeSave( info ) {
*/ //console.dir _.omit( info, 'mk' ), depth: null, colors: true
onBeforeSave: function( info ) { if ((info.ext !== 'html') && (info.ext !== 'pdf')) { return info.mk; }
let safe_eng = info.opts.pdf || 'wkhtmltopdf';
try { if (safe_eng === 'phantom') { safe_eng = 'phantomjs'; }
var safe_eng = info.opts.pdf || 'wkhtmltopdf'; if (_.has(engines, safe_eng)) {
if( safe_eng !== 'none' ) this.errHandler = info.opts.errHandler;
engines[ safe_eng ].call( this, info.mk, info.outputFile ); engines[ safe_eng ].call(this, info.mk, info.outputFile, info.opts, this.onError);
return null; // halt further processing return null; // halt further processing
} }
catch(ex) {
// { [Error: write EPIPE] code: 'EPIPE', errno: 'EPIPE', ... }
// { [Error: ENOENT] }
throw ( ex.inner && ex.inner.code === 'ENOENT' ) ?
{ fluenterror: this.codes.notOnPath, inner: ex.inner, engine: ex.cmd,
stack: ex.inner && ex.inner.stack } :
{ fluenterror: this.codes.pdfGeneration, inner: ex, stack: ex.stack };
}
} }
}); /* Low-level error callback for spawn(). May be called after HMR process
termination, so object references may not be valid here. That's okay; if
the references are invalid, the error was already logged. We could use
spawn-watch here but that causes issues on legacy Node.js. */
onError(ex, param) {
__guardMethod__(param.errHandler, 'err', o => o.err(HMSTATUS.pdfGeneration, ex));
}
}
module.exports = HtmlPdfCLIGenerator;
// TODO: Move each engine to a separate module // TODO: Move each engine to a separate module
var engines = { var engines = {
@ -73,13 +72,17 @@ Definition of the HtmlPdfCLIGenerator class.
TODO: If HTML generation has run, reuse that output TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease wkhtmltopdf rendering TODO: Local web server to ease wkhtmltopdf rendering
*/ */
wkhtmltopdf: function(markup, fOut) { wkhtmltopdf(markup, fOut, opts, on_error) {
// Save the markup to a temporary file // Save the markup to a temporary file
var tempFile = fOut.replace(/\.pdf$/i, '.pdf.html'); const tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8'); FS.writeFileSync(tempFile, markup, 'utf8');
var info = SPAWN( 'wkhtmltopdf', [ tempFile, fOut ] );
// Prepare wkhtmltopdf arguments.
let wkopts = _.extend({'margin-top': '10mm', 'margin-bottom': '10mm'}, opts.wkhtmltopdf);
wkopts = _.flatten(_.map(wkopts, (v, k) => [`--${k}`, v]));
const wkargs = wkopts.concat([ tempFile, fOut ]);
SPAWN('wkhtmltopdf', wkargs , false, on_error, this);
}, },
@ -91,23 +94,36 @@ Definition of the HtmlPdfCLIGenerator class.
TODO: If HTML generation has run, reuse that output TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering TODO: Local web server to ease Phantom rendering
*/ */
phantom: function( markup, fOut ) { phantomjs( markup, fOut, opts, on_error ) {
// Save the markup to a temporary file // Save the markup to a temporary file
var tempFile = fOut.replace(/\.pdf$/i, '.pdf.html'); const tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8'); FS.writeFileSync(tempFile, markup, 'utf8');
var scriptPath = SLASH( PATH.relative( process.cwd(), let scriptPath = PATH.relative(process.cwd(), PATH.resolve( __dirname, '../utils/rasterize.js' ));
PATH.resolve( __dirname, '../utils/rasterize.js' ) ) ); scriptPath = SLASH(scriptPath);
var sourcePath = SLASH( PATH.relative( process.cwd(), tempFile) ); const sourcePath = SLASH(PATH.relative( process.cwd(), tempFile));
var destPath = SLASH( PATH.relative( process.cwd(), fOut) ); const destPath = SLASH(PATH.relative( process.cwd(), fOut));
var info = SPAWN('phantomjs', [ scriptPath, sourcePath, destPath ]); SPAWN('phantomjs', [ scriptPath, sourcePath, destPath ], false, on_error, this);
},
/**
Generate a PDF from HTML using WeasyPrint's CLI interface.
Spawns a child process with `weasyprint <source> <target>`. Weasy Print
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
*/
weasyprint( markup, fOut, opts, on_error ) {
// Save the markup to a temporary file
const tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
SPAWN('weasyprint', [tempFile, fOut], false, on_error, this);
} }
}; };
function __guardMethod__(obj, methodName, transform) {
if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') {
}()); return transform(obj, methodName);
} else {
return undefined;
}
}

View File

@ -1,66 +1,58 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the HtmlPngGenerator class. Definition of the HtmlPngGenerator class.
@module generators/html-png-generator
@license MIT. See LICENSE.MD for details. @license MIT. See LICENSE.MD for details.
@module html-png-generator.js
*/ */
(function() { const TemplateGenerator = require('./template-generator');
const FS = require('fs-extra');
const SLASH = require('slash');
const SPAWN = require('../utils/safe-spawn');
var TemplateGenerator = require('./template-generator') const PATH = require('path');
, FS = require('fs-extra')
, HTML = require( 'html' );
/** /**
An HTML-based PNG resume generator for HackMyResume. An HTML-based PNG resume generator for HackMyResume.
*/ */
var HtmlPngGenerator = module.exports = TemplateGenerator.extend({ class HtmlPngGenerator extends TemplateGenerator {
constructor() { super('png', 'html'); }
invoke( /*rez, themeMarkup, cssInfo, opts*/ ) {}
init: function() {
this._super( 'png', 'html' );
},
invoke: function( rez, themeMarkup, cssInfo, opts ) {
// TODO: Not currently called or callable. // TODO: Not currently called or callable.
},
generate( rez, f, opts ) {
const htmlResults = opts.targets.filter(t => t.fmt.outFormat === 'html');
generate: function( rez, f, opts ) { const htmlFile = htmlResults[0].final.files.filter(fl => fl.info.ext === 'html');
var htmlResults = opts.targets.filter(function(t){ phantom(htmlFile[0].data, f);
return t.fmt.outFormat === 'html'; }
});
var htmlFile = htmlResults[0].final.files.filter(function(fl){
return fl.info.ext === 'html';
});
png( htmlFile[0].data, f );
} }
module.exports = HtmlPngGenerator;
});
/** /**
Generate a PNG from HTML. Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
*/ */
function png( markup, fOut ) {
// TODO: Which Webshot syntax?
// require('webshot')( markup , { encoding: 'binary', siteType: 'html' } )
// .pipe( FS.createWriteStream( fOut ) );
require('webshot')( markup , fOut, { siteType: 'html' }, function(err) { } );
}
var phantom = function( markup, fOut ) {
// Save the markup to a temporary file
}()); const tempFile = fOut.replace(/\.png$/i, '.png.html');
FS.writeFileSync(tempFile, markup, 'utf8');
const scriptPath = SLASH( PATH.relative( process.cwd(),
PATH.resolve( __dirname, '../utils/rasterize.js' ) ) );
const sourcePath = SLASH( PATH.relative( process.cwd(), tempFile) );
const destPath = SLASH( PATH.relative( process.cwd(), fOut) );
SPAWN('phantomjs', [ scriptPath, sourcePath, destPath ]);
};

View File

@ -1,36 +1,33 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the JsonGenerator class. Definition of the JsonGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module generators/json-generator
@module json-generator.js @license MIT. See LICENSE.md for details.
*/ */
var BaseGenerator = require('./base-generator'); const BaseGenerator = require('./base-generator');
var FS = require('fs'); const FS = require('fs');
var _ = require('underscore'); const FJCV = require('fresh-jrs-converter');
/** /** The JsonGenerator generates a FRESH or JRS resume as an output. */
The JsonGenerator generates a JSON resume directly.
*/
var JsonGenerator = module.exports = BaseGenerator.extend({
init: function(){ class JsonGenerator extends BaseGenerator {
this._super( 'json' );
},
invoke: function( rez ) { constructor() { super('json'); }
// TODO: merge with FCVD
function replacer( key,value ) { // Exclude these keys from stringification invoke( rez ) {
return _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index', let altRez = FJCV[ `to${rez.format() === 'FRESH' ? 'JRS' : 'FRESH'}` ](rez);
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result', return altRez = FJCV.toSTRING( altRez );
'isModified', 'htmlPreview', 'safe' ],
function( val ) { return key.trim() === val; }
) ? undefined : value;
} }
return JSON.stringify( rez, replacer, 2 ); //altRez.stringify()
},
generate: function( rez, f ) { generate( rez, f ) {
FS.writeFileSync(f, this.invoke(rez), 'utf8'); FS.writeFileSync(f, this.invoke(rez), 'utf8');
} }
}
}); module.exports = JsonGenerator;

View File

@ -1,14 +1,21 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the JsonYamlGenerator class. Definition of the JsonYamlGenerator class.
@module json-yaml-generator.js @module generators/json-yaml-generator
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
*/ */
(function() {
var BaseGenerator = require('./base-generator');
var FS = require('fs'); const BaseGenerator = require('./base-generator');
var YAML = require('yamljs'); const FS = require('fs');
const YAML = require('yamljs');
/** /**
JsonYamlGenerator takes a JSON resume object and translates it directly to JsonYamlGenerator takes a JSON resume object and translates it directly to
@ -16,22 +23,19 @@ Definition of the JsonYamlGenerator class.
also YamlGenerator (yaml-generator.js). also YamlGenerator (yaml-generator.js).
*/ */
var JsonYamlGenerator = module.exports = BaseGenerator.extend({ class JsonYamlGenerator extends BaseGenerator {
init: function(){ constructor() { super('yml'); }
this._super( 'yml' );
},
invoke: function( rez, themeMarkup, cssInfo, opts ) { invoke( rez/*, themeMarkup, cssInfo, opts*/ ) {
return YAML.stringify(JSON.parse( rez.stringify() ), Infinity, 2); return YAML.stringify(JSON.parse( rez.stringify() ), Infinity, 2);
},
generate: function( rez, f, opts ) {
var data = YAML.stringify( JSON.parse( rez.stringify() ), Infinity, 2 );
FS.writeFileSync( f, data, 'utf8' );
} }
generate( rez, f/*, opts */) {
const data = YAML.stringify(JSON.parse( rez.stringify() ), Infinity, 2);
FS.writeFileSync(f, data, 'utf8');
return data;
}
}
}); module.exports = JsonYamlGenerator;
}());

View File

@ -1,18 +1,17 @@
/** /**
Definition of the LaTeXGenerator class. Definition of the LaTeXGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module generators/latex-generator
@module latex-generator.js @license MIT. See LICENSE.md for details.
*/ */
var TemplateGenerator = require('./template-generator'); const TemplateGenerator = require('./template-generator');
/** /**
LaTeXGenerator generates a LaTeX resume via TemplateGenerator. LaTeXGenerator generates a LaTeX resume via TemplateGenerator.
*/ */
var LaTeXGenerator = module.exports = TemplateGenerator.extend({ class LaTeXGenerator extends TemplateGenerator {
init: function(){ constructor() { super('latex', 'tex'); }
this._super( 'latex', 'tex' );
} }
}); module.exports = LaTeXGenerator;

View File

@ -1,18 +1,17 @@
/** /**
Definition of the MarkdownGenerator class. Definition of the MarkdownGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module generators/markdown-generator
@module markdown-generator.js @license MIT. See LICENSE.md for details.
*/ */
var TemplateGenerator = require('./template-generator'); const TemplateGenerator = require('./template-generator');
/** /**
MarkdownGenerator generates a Markdown-formatted resume via TemplateGenerator. MarkdownGenerator generates a Markdown-formatted resume via TemplateGenerator.
*/ */
var MarkdownGenerator = module.exports = TemplateGenerator.extend({ class MarkdownGenerator extends TemplateGenerator {
init: function(){ constructor() { super('md', 'txt'); }
this._super( 'md', 'txt' );
} }
}); module.exports = MarkdownGenerator;

View File

@ -1,26 +1,25 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the TemplateGenerator class. TODO: Refactor Definition of the TemplateGenerator class. TODO: Refactor
@module generators/template-generator
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@module template-generator.js
*/ */
(function() { const FS = require('fs-extra');
const _ = require('underscore');
const MD = require('marked');
const XML = require('xml-escape');
var FS = require( 'fs-extra' ) const PATH = require('path');
, _ = require( 'underscore' ) const parsePath = require('parse-filepath');
, MD = require( 'marked' ) const MKDIRP = require('mkdirp');
, XML = require( 'xml-escape' ) const BaseGenerator = require('./base-generator');
, PATH = require('path') const EXTEND = require('extend');
, parsePath = require('parse-filepath')
, MKDIRP = require('mkdirp')
, BaseGenerator = require( './base-generator' )
, EXTEND = require('extend')
, FRESHTheme = require('../core/fresh-theme')
, JRSTheme = require('../core/jrs-theme');
@ -30,7 +29,8 @@ Definition of the TemplateGenerator class. TODO: Refactor
plain text, and XML versions of Microsoft Word, Excel, and OpenOffice. plain text, and XML versions of Microsoft Word, Excel, and OpenOffice.
@class TemplateGenerator @class TemplateGenerator
*/ */
var TemplateGenerator = module.exports = BaseGenerator.extend({
class TemplateGenerator extends BaseGenerator {
@ -38,10 +38,10 @@ Definition of the TemplateGenerator class. TODO: Refactor
generator. Will usually be called by a derived generator such as generator. Will usually be called by a derived generator such as
HTMLGenerator or MarkdownGenerator. */ HTMLGenerator or MarkdownGenerator. */
init: function( outputFormat, templateFormat, cssFile ){ constructor( outputFormat, templateFormat/*, cssFile */) {
this._super( outputFormat ); super(outputFormat);
this.tplFormat = templateFormat || outputFormat; this.tplFormat = templateFormat || outputFormat;
}, }
@ -53,34 +53,39 @@ Definition of the TemplateGenerator class. TODO: Refactor
@returns {Array} An array of objects representing the generated output @returns {Array} An array of objects representing the generated output
files. */ files. */
invoke: function( rez, opts ) { invoke( rez, opts ) {
opts = opts ? opts =
(this.opts = EXTEND( true, { }, _defaultOpts, opts )) : opts
this.opts; ? (this.opts = EXTEND( true, { }, _defaultOpts, opts ))
: this.opts;
// Sort such that CSS files are processed before others // Sort such that CSS files are processed before others
var curFmt = opts.themeObj.getFormat( this.format ); const curFmt = opts.themeObj.getFormat( this.format );
curFmt.files = _.sortBy( curFmt.files, function(fi) { curFmt.files = _.sortBy(curFmt.files, fi => fi.ext !== 'css');
return fi.ext !== 'css';
});
// Run the transformation! // Run the transformation!
var results = curFmt.files.map( function( tplInfo, idx ) { const results = curFmt.files.map(function( tplInfo, idx ) {
var trx = this.single( rez, tplInfo.data, this.format, opts, opts.themeObj, curFmt ); let trx;
if( tplInfo.ext === 'css' ) { curFmt.files[idx].data = trx; } if (tplInfo.action === 'transform') {
else if( tplInfo.ext === 'html' ) { trx = this.transform(rez, tplInfo.data, this.format, opts, opts.themeObj, curFmt);
if (tplInfo.ext === 'css') {
curFmt.files[idx].data = trx;
} else { tplInfo.ext === 'html'; }
}
//tplInfo.css contains the CSS data loaded by theme //tplInfo.css contains the CSS data loaded by theme
//tplInfo.cssPath contains the absolute path to the source CSS File //tplInfo.cssPath contains the absolute path to the source CSS File
//else {}
// Images and non-transformable binary files
if (typeof opts.onTransform === 'function') {
opts.onTransform(tplInfo);
} }
return {info: tplInfo, data: trx}; return {info: tplInfo, data: trx};
}, this); }
, this);
return { return {files: results};
files: results }
};
},
@ -91,61 +96,71 @@ Definition of the TemplateGenerator class. TODO: Refactor
@param f Full path to the output resume file to generate. @param f Full path to the output resume file to generate.
@param opts Generator options. */ @param opts Generator options. */
generate: function( rez, f, opts ) { generate( rez, f, opts ) {
// Prepare // Prepare
this.opts = EXTEND(true, { }, _defaultOpts, opts); this.opts = EXTEND(true, { }, _defaultOpts, opts);
// Call the string-based generation method to perform the generation. // Call the string-based generation method
var genInfo = this.invoke( rez, null ); const genInfo = this.invoke(rez, null);
const outFolder = parsePath( f ).dirname;
var outFolder = parsePath( f ).dirname; const curFmt = opts.themeObj.getFormat(this.format);
var curFmt = opts.themeObj.getFormat( this.format );
// Process individual files within this format. For example, the HTML // Process individual files within this format. For example, the HTML
// output format for a theme may have multiple HTML files, CSS files, // output format for a theme may have multiple HTML files, CSS files,
// etc. Process them here. // etc. Process them here.
genInfo.files.forEach(function( file ) { genInfo.files.forEach(function( file ) {
// console.dir _.omit(file.info,'cssData','data','css' )
// Pre-processing // Pre-processing
file.info.orgPath = file.info.orgPath || ''; // <-- For JRS themes file.info.orgPath = file.info.orgPath || '';
var thisFilePath = PATH.join( outFolder, file.info.orgPath ); const thisFilePath =
if( this.onBeforeSave ) { file.info.primary
? f
: PATH.join(outFolder, file.info.orgPath);
if ((file.info.action !== 'copy') && this.onBeforeSave) {
file.data = this.onBeforeSave({ file.data = this.onBeforeSave({
theme: opts.themeObj, theme: opts.themeObj,
outputFile: (file.info.major ? f : thisFilePath), outputFile: thisFilePath,
mk: file.data, mk: file.data,
opts: this.opts opts: this.opts,
ext: file.info.ext
}); });
if( !file.data ) return; // PDF etc if (!file.data) {
return;
}
} }
// Write the file // Write the file
var fileName = file.info.major ? f : thisFilePath; if (typeof opts.beforeWrite === 'function') {
MKDIRP.sync( PATH.dirname( fileName ) ); opts.beforeWrite({data: thisFilePath});
FS.writeFileSync( fileName, file.data, }
{ encoding: 'utf8', flags: 'w' } ); MKDIRP.sync(PATH.dirname( thisFilePath ));
// Post-processing if (file.info.action !== 'copy') {
this.onAfterSave && this.onAfterSave( FS.writeFileSync(thisFilePath, file.data, {encoding: 'utf8', flags: 'w'});
{ outputFile: fileName, mk: file.data, opts: this.opts } ); } else {
FS.copySync(file.info.path, thisFilePath);
}, this); }
if (typeof opts.afterWrite === 'function') {
// Some themes require a symlink structure. If so, create it. opts.afterWrite({data: thisFilePath});
if( curFmt.symLinks ) {
Object.keys( curFmt.symLinks ).forEach( function(loc) {
var absLoc = PATH.join(outFolder, loc);
var absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
// 'file', 'dir', or 'junction' (Windows only)
var type = parsePath( absLoc ).extname ? 'file' : 'junction';
FS.symlinkSync( absTarg, absLoc, type);
});
} }
return genInfo; // Post-processing
if (this.onAfterSave) {
return this.onAfterSave({outputFile: thisFilePath, mk: file.data, opts: this.opts});
}
}
}, , this);
// Some themes require a symlink structure. If so, create it.
createSymLinks(curFmt, outFolder);
return genInfo;
}
@ -157,42 +172,69 @@ Definition of the TemplateGenerator class. TODO: Refactor
@param cssInfo Needs to be refactored. @param cssInfo Needs to be refactored.
@param opts Options and passthrough data. */ @param opts Options and passthrough data. */
single: function( json, jst, format, opts, theme, curFmt ) { transform( json, jst, format, opts, theme, curFmt ) {
this.opts.freezeBreaks && ( jst = freeze(jst) ); if (this.opts.freezeBreaks) {
jst = freeze(jst);
var eng = require( '../renderers/' + theme.engine + '-generator' ); }
var result = eng.generate( json, jst, format, curFmt, opts, theme ); const eng = require(`../renderers/${theme.engine}-generator`);
let result = eng.generate(json, jst, format, curFmt, opts, theme);
this.opts.freezeBreaks && ( result = unfreeze(result) ); if (this.opts.freezeBreaks) {
result = unfreeze(result);
}
return result; return result;
} }
}
});
/** Export the TemplateGenerator function/ctor. */
module.exports = TemplateGenerator; module.exports = TemplateGenerator;
var createSymLinks = function( curFmt, outFolder ) {
// Some themes require a symlink structure. If so, create it.
if (curFmt.symLinks) {
Object.keys( curFmt.symLinks ).forEach(function(loc) {
const absLoc = PATH.join(outFolder, loc);
const absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
// Set type to 'file', 'dir', or 'junction' (Windows only)
const type = parsePath( absLoc ).extname ? 'file' : 'junction';
try {
return FS.symlinkSync(absTarg, absLoc, type);
} catch (err) {
let succeeded = false;
if (err.code === 'EEXIST') {
FS.unlinkSync(absLoc);
try {
FS.symlinkSync(absTarg, absLoc, type);
succeeded = true;
} catch (error) {
throw error;
}
}
if (!succeeded) {
throw err;
}
}
});
return;
}
};
/** Freeze newlines for protection against errant JST parsers. */ /** Freeze newlines for protection against errant JST parsers. */
function freeze( markup ) { var freeze = function( markup ) {
return markup markup.replace( _reg.regN, _defaultOpts.nSym );
.replace( _reg.regN, _defaultOpts.nSym ) return markup.replace( _reg.regR, _defaultOpts.rSym );
.replace( _reg.regR, _defaultOpts.rSym ); };
}
/** Unfreeze newlines when the coast is clear. */ /** Unfreeze newlines when the coast is clear. */
function unfreeze( markup ) { var unfreeze = function( markup ) {
return markup markup.replace(_reg.regSymR, '\r');
.replace( _reg.regSymR, '\r' ) return markup.replace(_reg.regSymN, '\n');
.replace( _reg.regSymN, '\n' ); };
}
@ -205,40 +247,37 @@ Definition of the TemplateGenerator class. TODO: Refactor
rSym: '&retn;', // return entity rSym: '&retn;', // return entity
template: { template: {
interpolate: /\{\{(.+?)\}\}/g, interpolate: /\{\{(.+?)\}\}/g,
escape: /\{\{\=(.+?)\}\}/g, escape: /\{\{=(.+?)\}\}/g,
evaluate: /\{\%(.+?)\%\}/g, evaluate: /\{%(.+?)%\}/g,
comment: /\{\#(.+?)\#\}/g comment: /\{#(.+?)#\}/g
}, },
filters: { filters: {
out: function( txt ) { return txt; }, out( txt ) { return txt; },
raw: function( txt ) { return txt; }, raw( txt ) { return txt; },
xml: function( txt ) { return XML(txt); }, xml( txt ) { return XML(txt); },
md: function( txt ) { return MD( txt || '' ); }, md( txt ) { return MD( txt || '' ); },
mdin: function( txt ) { mdin( txt ) { return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); },
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); lower( txt ) { return txt.toLowerCase(); },
}, link( name, url ) {
lower: function( txt ) { return txt.toLowerCase(); }, if (url) { return `<a href="${url}">${name}</a>`; } else { return name; }
link: function( name, url ) { return url ? }
'<a href="' + url + '">' + name + '</a>' : name; }
}, },
prettify: { // ← See https://github.com/beautify-web/js-beautify#options prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2, indent_size: 2,
unformatted: ['em','strong','a'], unformatted: ['em','strong','a'],
max_char: 80, // ← See lib/html.js in above-linked repo max_char: 80
//wrap_line_length: 120, <-- Don't use this } // ← See lib/html.js in above-linked repo
}
}; };
//wrap_line_length: 120, <-- Don't use this
/** Regexes for linebreak preservation. */ /** Regexes for linebreak preservation. */
/* eslint-disable no-control-regex */
var _reg = { var _reg = {
regN: new RegExp( '\n', 'g' ), regN: new RegExp( '\n', 'g' ),
regR: new RegExp( '\r', 'g' ), regR: new RegExp( '\r', 'g' ),
regSymN: new RegExp( _defaultOpts.nSym, 'g' ), regSymN: new RegExp( _defaultOpts.nSym, 'g' ),
regSymR: new RegExp( _defaultOpts.rSym, 'g' ) regSymR: new RegExp( _defaultOpts.rSym, 'g' )
}; };
/* eslint-enable no-control-regex */
}());

View File

@ -1,20 +1,16 @@
/** /**
Definition of the TextGenerator class. Definition of the TextGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module generators/text-generator
@module text-generator.js @license MIT. See LICENSE.md for details.
*/ */
var TemplateGenerator = require('./template-generator'); const TemplateGenerator = require('./template-generator');
/** /**
The TextGenerator generates a plain-text resume via the TemplateGenerator. The TextGenerator generates a plain-text resume via the TemplateGenerator.
*/ */
var TextGenerator = TemplateGenerator.extend({ class TextGenerator extends TemplateGenerator {
constructor() { super('txt'); }
init: function(){ }
this._super( 'txt' );
},
});
module.exports = TextGenerator; module.exports = TextGenerator;

View File

@ -1,19 +1,14 @@
/** /*
Definition of the WordGenerator class. Definition of the WordGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @module generators/word-generator
@module word-generator.js @license MIT. See LICENSE.md for details.
*/ */
(function() {
var TemplateGenerator = require('./template-generator'); const TemplateGenerator = require('./template-generator');
var WordGenerator = module.exports = TemplateGenerator.extend({
init: function(){ class WordGenerator extends TemplateGenerator {
this._super( 'doc', 'xml' ); constructor() { super('doc', 'xml'); }
} }
}); module.exports = WordGenerator;
}());

View File

@ -1,18 +1,14 @@
/** /**
Definition of the XMLGenerator class. Definition of the XMLGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
@module xml-generator.js @module generatprs/xml-generator
*/ */
var BaseGenerator = require('./base-generator'); const BaseGenerator = require('./base-generator');
/** /** The XmlGenerator generates an XML resume via the TemplateGenerator. */
The XmlGenerator generates an XML resume via the TemplateGenerator. class XMLGenerator extends BaseGenerator {
*/ constructor() { super('xml'); }
var XMLGenerator = module.exports = BaseGenerator.extend({ }
init: function(){ module.exports = XMLGenerator;
this._super( 'xml' );
},
});

View File

@ -1,24 +1,19 @@
/** /**
Definition of the YAMLGenerator class. Definition of the YAMLGenerator class.
@module yaml-generator.js @module yaml-generator.js
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
*/ */
(function() { const TemplateGenerator = require('./template-generator');
var TemplateGenerator = require('./template-generator');
/** /**
YamlGenerator generates a YAML-formatted resume via TemplateGenerator. YamlGenerator generates a YAML-formatted resume via TemplateGenerator.
*/ */
var YAMLGenerator = module.exports = TemplateGenerator.extend({ class YAMLGenerator extends TemplateGenerator {
constructor() { super('yml', 'yml'); }
init: function(){
this._super( 'yml', 'yml' );
} }
});
}()); module.exports = YAMLGenerator;

View File

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

View File

@ -0,0 +1,77 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Block helper definitions for HackMyResume / FluentCV.
@license MIT. See LICENSE.md for details.
@module helpers/generic-helpers
*/
const LO = require('lodash');
const _ = require('underscore');
require('../utils/string');
/** Block helper function definitions. */
module.exports = {
/**
Emit the enclosed content if the resume has a section with
the specified name. Otherwise, emit an empty string ''.
*/
section( title, options ) {
title = title.trim().toLowerCase();
const obj = LO.get(this.r, title);
let ret = '';
if (obj) {
if (_.isArray(obj)) {
if (obj.length) {
ret = options.fn(this);
}
} else if (_.isObject(obj)) {
if ((obj.history && obj.history.length) || (obj.sets && obj.sets.length)) {
ret = options.fn(this);
}
}
}
return ret;
},
ifHasSkill( rez, skill, options ) {
const skUp = skill.toUpperCase();
const ret = _.some(rez.skills.list, sk => (skUp.toUpperCase() === sk.name.toUpperCase()) && sk.years
, this);
if (ret) { return options.fn(this); }
},
/**
Emit the enclosed content if the resume has the named
property or subproperty.
*/
has( title, options ) {
title = title && title.trim().toLowerCase();
if (LO.get(this.r, title)) {
return options.fn(this);
}
},
/**
Return true if either value is truthy.
@method either
*/
either( lhs, rhs, options ) { if (lhs || rhs) { return options.fn(this); } }
};

View File

@ -1,67 +1,67 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Generic template helper definitions for command-line output. Generic template helper definitions for command-line output.
@module console-helpers.js @module console-helpers.js
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
*/ */
(function() {
const PAD = require('string-padding');
var PAD = require('string-padding') const LO = require('lodash');
, LO = require('lodash') const CHALK = require('chalk');
, CHALK = require('chalk') const _ = require('underscore');
, _ = require('underscore');
require('../utils/string'); require('../utils/string');
var consoleFormatHelpers = module.exports = { module.exports = {
v: function( val, defaultVal, padding, style ) { v( val, defaultVal, padding, style ) {
retVal = ( val === null || val === undefined ) ? defaultVal : val; let retVal = ( (val === null) || (val === undefined) ) ? defaultVal : val;
var spaces = 0; let spaces = 0;
if (String.is(padding)) { if (String.is(padding)) {
spaces = parseInt(padding, 10); spaces = parseInt(padding, 10);
if( isNaN(spaces) ) spaces = 0; if (isNaN(spaces)) { spaces = 0; }
} } else if (_.isNumber(padding)) {
else if( _.isNumber(padding) ) {
spaces = padding; spaces = padding;
} }
if( spaces !== 0 ) if (spaces !== 0) {
retVal = PAD(retVal, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT); retVal = PAD(retVal, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT);
}
if (style && String.is( style )) { if (style && String.is( style )) {
retVal = LO.get( CHALK, style )( retVal ); retVal = LO.get( CHALK, style )( retVal );
} }
return retVal; return retVal;
}, },
gapLength: function(val) { gapLength(val) {
if( val < 35 ) if (val < 35) {
return CHALK.green.bold(val); return CHALK.green.bold(val);
else if( val < 95 ) } else if (val < 95) {
return CHALK.yellow.bold(val); return CHALK.yellow.bold(val);
else } else {
return CHALK.red.bold(val); return CHALK.red.bold(val);
}
}, },
style: function( val, style ) { style( val, style ) {
return LO.get( CHALK, style )( val ); return LO.get( CHALK, style )( val );
}, },
isPlural: function( val, options ) { isPlural( val, options ) {
if( val > 1 ) if (val > 1) {
return options.fn(this); return options.fn(this);
}
}, },
pad: function( val, spaces ) { pad( val, spaces ) {
return PAD(val, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT); return PAD(val, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT);
} }
}; };
}());

View File

@ -1,3 +1,9 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Generic template helper definitions for HackMyResume / FluentCV. Generic template helper definitions for HackMyResume / FluentCV.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@ -5,104 +11,151 @@ Generic template helper definitions for HackMyResume / FluentCV.
*/ */
(function() {
var MD = require('marked') const MD = require('marked');
, H2W = require('../utils/html-to-wpml') const H2W = require('../utils/html-to-wpml');
, XML = require('xml-escape') const XML = require('xml-escape');
, FluentDate = require('../core/fluent-date') const FluentDate = require('../core/fluent-date');
, HMSTATUS = require('../core/status-codes') const HMSTATUS = require('../core/status-codes');
, moment = require('moment') const moment = require('moment');
, FS = require('fs') const FS = require('fs');
, LO = require('lodash') const LO = require('lodash');
, PATH = require('path') const PATH = require('path');
, printf = require('printf') const printf = require('printf');
, _ = require('underscore') const _ = require('underscore');
, unused = require('../utils/string'); require('../utils/string');
/** Generic template helper function definitions. */ /** Generic template helper function definitions. */
var GenericHelpers = module.exports = { var GenericHelpers = (module.exports = {
/** /**
Convert the input date to a specified format through Moment.js. Emit a formatted string representing the specified datetime.
If date is invalid, will return the time provided by the user, Convert the input date to the specified format through Moment.js. If date is
or default to the fallback param or 'Present' if that is set to true valid, return the formatted date string. If date is null, undefined, or other
@method formatDate falsy value, return the value of the 'fallback' parameter, if specified, or
null if no fallback was specified. If date is invalid, but not null/undefined/
falsy, return it as-is.
@param {string|Moment} datetime A date value.
@param {string} [dtFormat='YYYY-MM'] The desired datetime format. Must be a
Moment.js-compatible datetime format.
@param {string|Moment} fallback A fallback value to use if the specified date
is null, undefined, or falsy.
*/ */
formatDate: function(datetime, format, fallback) { formatDate(datetime, dtFormat, fallback) {
if (moment) {
var momentDate = moment( datetime ); if (datetime == null) { datetime = undefined; }
if (momentDate.isValid()) return momentDate.format(format); if (dtFormat == null) { dtFormat = 'YYYY-MM'; }
// If a Moment.js object was passed in, just call format on it
if (datetime && moment.isMoment(datetime)) {
return datetime.format(dtFormat);
} }
return datetime || (typeof fallback == 'string' ? fallback : (fallback === true ? 'Present' : null)); if (String.is(datetime)) {
// If a string was passed in, convert to Moment using the 2-paramter
// constructor with an explicit format string.
let momentDate = moment(datetime, dtFormat);
if (momentDate.isValid()) { return momentDate.format(dtFormat); }
// If that didn't work, try again with the single-parameter constructor
// but this may throw a deprecation warning
momentDate = moment(datetime);
if (momentDate.isValid()) { return momentDate.format(dtFormat); }
}
// We weren't able to format the provided datetime. Now do one of three
// things.
// 1. If datetime is non-null/non-falsy, return it. For this helper,
// string date values that we can't parse are assumed to be display dates.
// 2. If datetime IS null or falsy, use the value from the fallback.
// 3. If the fallback value is specifically 'true', emit 'Present'.
return datetime ||
(typeof fallback === 'string'
? fallback
: (fallback === true ? 'Present' : ''));
}, },
/**
Emit a formatted string representing the specified datetime.
@param {string} dateValue A raw date value from the FRESH or JRS resume.
@param {string} [dateFormat='YYYY-MM'] The desired datetime format. Must be
compatible with Moment.js datetime formats.
@param {string} [dateDefault=null] The default date value to use if the dateValue
parameter is null, undefined, or falsy.
*/
date(dateValue, dateFormat, dateDefault) {
if (!dateDefault || !String.is(dateDefault)) { dateDefault = 'Current'; }
if (!dateFormat || !String.is(dateFormat)) { dateFormat = 'YYYY-MM'; }
if (!dateValue || !String.is(dateValue)) { dateValue = null; }
if (!dateValue) { return dateDefault; }
const reserved = ['current', 'present', 'now'];
const dateValueSafe = dateValue.trim().toLowerCase();
if (_.contains(reserved, dateValueSafe)) { return dateValue; }
const dateValueMoment = moment(dateValue, dateFormat);
if (dateValueMoment.isValid()) { return dateValueMoment.format(dateFormat); }
return dateValue;
},
/** /**
Given a resume sub-object with a start/end date, format a representation of Given a resume sub-object with a start/end date, format a representation of
the date range. the date range.
@method dateRange
*/ */
dateRange: function( obj, fmt, sep, fallback, options ) { dateRange( obj, fmt, sep, fallback ) {
if( !obj ) return ''; if (!obj) { return ''; }
return _fromTo( obj.start, obj.end, fmt, sep, fallback, options ); return _fromTo(obj.start, obj.end, fmt, sep, fallback);
}, },
/** /**
Format a from/to date range for display. Format a from/to date range for display.
@method toFrom @method toFrom
*/ */
fromTo: function() { fromTo() { return _fromTo.apply(this, arguments); },
return _fromTo.apply( this, arguments );
},
/** /**
Return a named color value as an RRGGBB string. Return a named color value as an RRGGBB string.
@method toFrom @method toFrom
*/ */
color: function( colorName, colorDefault ) { color( colorName, colorDefault ) {
// Key must be specified // Key must be specified
if (!(colorName && colorName.trim())) { if (!(colorName && colorName.trim())) {
_reportError( HMSTATUS.invalidHelperUse, { return _reportError(HMSTATUS.invalidHelperUse,
helper: 'fontList', error: HMSTATUS.missingParam, expected: 'name' {helper: 'fontList', error: HMSTATUS.missingParam, expected: 'name'});
}); } else {
} if (!GenericHelpers.theme.colors) { return colorDefault; }
else { const ret = GenericHelpers.theme.colors[ colorName ];
if( !GenericHelpers.theme.colors ) return colorDefault; if (!(ret && ret.trim())) {
var ret = GenericHelpers.theme.colors[ colorName ];
if( !(ret && ret.trim()) )
return colorDefault; return colorDefault;
}
return ret; return ret;
} }
}, },
/**
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;
}
},
/** /**
Emit the size of the specified named font. Emit the size of the specified named font.
@param key {String} A named style from the "fonts" section of the theme's @param key {String} A named style from the "fonts" section of the theme's
theme.json file. For example: 'default' or 'heading1'. theme.json file. For example: 'default' or 'heading1'.
*/ */
fontSize: function( key, defSize, units ){ fontSize( key, defSize/*, units*/ ) {
var ret = '' let ret = '';
, hasDef = defSize && ( String.is( defSize ) || _.isNumber( defSize )); const hasDef = defSize && ( String.is( defSize ) || _.isNumber( defSize ));
// Key must be specified // Key must be specified
if (!(key && key.trim())) { if (!(key && key.trim())) {
@ -110,28 +163,26 @@ Generic template helper definitions for HackMyResume / FluentCV.
helper: 'fontSize', error: HMSTATUS.missingParam, expected: 'key' helper: 'fontSize', error: HMSTATUS.missingParam, expected: 'key'
}); });
return ret; return ret;
}
else if ( GenericHelpers.theme.fonts ) { } else if (GenericHelpers.theme.fonts) {
var fontSpec = LO.get( GenericHelpers.theme.fonts, this.format + '.' + key ); let fontSpec = LO.get( GenericHelpers.theme.fonts, this.format + '.' + key );
if (!fontSpec) { if (!fontSpec) {
// Check for an "all" format // Check for an "all" format
if( GenericHelpers.theme.fonts.all ) if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[ key ]; fontSpec = GenericHelpers.theme.fonts.all[ key ];
} }
}
if( fontSpec ) { if( fontSpec ) {
// fontSpec can be a string, an array, or an object // fontSpec can be a string, an array, or an object
if( String.is( fontSpec )) { if( String.is( fontSpec )) {
// No font size was specified, only a font family. // No font size was specified, only a font family.
} } else if( _.isArray( fontSpec )) {
else if( _.isArray( fontSpec )) {
// An array of fonts were specified. Each one could be a string // An array of fonts were specified. Each one could be a string
// or an object // or an object
if( !String.is( fontSpec[0] )) { if( !String.is( fontSpec[0] )) {
ret = fontSpec[0].size; ret = fontSpec[0].size;
} }
} } else {
else {
// A font description object. // A font description object.
ret = fontSpec.size; ret = fontSpec.size;
} }
@ -140,9 +191,9 @@ Generic template helper definitions for HackMyResume / FluentCV.
// We weren't able to lookup the specified key. Default to defFont. // We weren't able to lookup the specified key. Default to defFont.
if (!ret) { if (!ret) {
if( hasDef ) if (hasDef) {
ret = defSize; ret = defSize;
else { } else {
_reportError( HMSTATUS.invalidHelperUse, { _reportError( HMSTATUS.invalidHelperUse, {
helper: 'fontSize', error: HMSTATUS.missingParam, helper: 'fontSize', error: HMSTATUS.missingParam,
expected: 'defSize'}); expected: 'defSize'});
@ -151,9 +202,10 @@ Generic template helper definitions for HackMyResume / FluentCV.
} }
return ret; return ret;
}, },
/** /**
Emit the font face (such as 'Helvetica' or 'Calibri') associated with the Emit the font face (such as 'Helvetica' or 'Calibri') associated with the
provided key. provided key.
@ -162,10 +214,10 @@ Generic template helper definitions for HackMyResume / FluentCV.
@param defFont {String} The font to use if the specified key isn't present. @param defFont {String} The font to use if the specified key isn't present.
Can be any valid font-face name such as 'Helvetica Neue' or 'Calibri'. Can be any valid font-face name such as 'Helvetica Neue' or 'Calibri'.
*/ */
fontFace: function( key, defFont ) { fontFace( key, defFont ) {
var ret = '' let ret = '';
, hasDef = defFont && String.is( defFont ); const hasDef = defFont && String.is( defFont );
// Key must be specified // Key must be specified
if (!( key && key.trim())) { if (!( key && key.trim())) {
@ -173,33 +225,33 @@ Generic template helper definitions for HackMyResume / FluentCV.
helper: 'fontFace', error: HMSTATUS.missingParam, expected: 'key' helper: 'fontFace', error: HMSTATUS.missingParam, expected: 'key'
}); });
return ret; return ret;
}
// If the theme has a "fonts" section, lookup the font face. // If the theme has a "fonts" section, lookup the font face.
else if( GenericHelpers.theme.fonts ) { } else if( GenericHelpers.theme.fonts ) {
var fontSpec = LO.get( GenericHelpers.theme.fonts, this.format + '.' + key ); let fontSpec = LO.get( GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) { if (!fontSpec) {
// Check for an "all" format // Check for an "all" format
if( GenericHelpers.theme.fonts.all ) if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[ key ]; fontSpec = GenericHelpers.theme.fonts.all[ key ];
} }
}
if (fontSpec) { if (fontSpec) {
// fontSpec can be a string, an array, or an object // fontSpec can be a string, an array, or an object
if (String.is(fontSpec)) { if (String.is(fontSpec)) {
ret = fontSpec; ret = fontSpec;
} } else if (_.isArray(fontSpec)) {
else if( _.isArray( fontSpec )) {
// An array of fonts were specified. Each one could be a string // An array of fonts were specified. Each one could be a string
// or an object // or an object
ret = String.is( fontSpec[0] ) ? fontSpec[0] : fontSpec[0].name; ret = String.is( fontSpec[0] ) ? fontSpec[0] : fontSpec[0].name;
} } else {
else {
// A font description object. // A font description object.
ret = fontSpec.name; ret = fontSpec.name;
} }
} }
} }
// We weren't able to lookup the specified key. Default to defFont. // We weren't able to lookup the specified key. Default to defFont.
if (!(ret && ret.trim())) { if (!(ret && ret.trim())) {
ret = defFont; ret = defFont;
@ -214,6 +266,8 @@ Generic template helper definitions for HackMyResume / FluentCV.
return ret; return ret;
}, },
/** /**
Emit a comma-delimited list of font names suitable associated with the Emit a comma-delimited list of font names suitable associated with the
provided key. provided key.
@ -225,44 +279,39 @@ Generic template helper definitions for HackMyResume / FluentCV.
@param sep {String} The default separator to use in the rendered output. @param sep {String} The default separator to use in the rendered output.
Defaults to ", " (comma with a space). Defaults to ", " (comma with a space).
*/ */
fontList: function( key, defFontList, sep ) { fontList( key, defFontList, sep ) {
var ret = '' let ret = '';
, hasDef = defFontList && String.is( defFontList ); const hasDef = defFontList && String.is( defFontList );
// Key must be specified // Key must be specified
if (!( key && key.trim())) { if (!( key && key.trim())) {
_reportError( HMSTATUS.invalidHelperUse, { _reportError( HMSTATUS.invalidHelperUse, {
helper: 'fontList', error: HMSTATUS.missingParam, expected: 'key' helper: 'fontList', error: HMSTATUS.missingParam, expected: 'key'
}); });
}
// If the theme has a "fonts" section, lookup the font list. // If the theme has a "fonts" section, lookup the font list.
else if( GenericHelpers.theme.fonts ) { } else if (GenericHelpers.theme.fonts) {
var fontSpec = LO.get( GenericHelpers.theme.fonts, this.format + '.' + key ); let fontSpec = LO.get(GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) { if (!fontSpec) {
if( GenericHelpers.theme.fonts.all ) if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[ key ]; fontSpec = GenericHelpers.theme.fonts.all[ key ];
} }
}
if (fontSpec) { if (fontSpec) {
// fontSpec can be a string, an array, or an object // fontSpec can be a string, an array, or an object
if (String.is(fontSpec)) { if (String.is(fontSpec)) {
ret = fontSpec; ret = fontSpec;
} } else if (_.isArray(fontSpec)) {
else if( _.isArray( fontSpec )) {
// An array of fonts were specified. Each one could be a string // An array of fonts were specified. Each one could be a string
// or an object // or an object
fontSpec = fontSpec.map( function( ff ) { fontSpec = fontSpec.map( ff => `'${String.is( ff ) ? ff : ff.name}'`);
return "'" + (String.is( ff ) ? ff : ff.name) + "'";
});
ret = fontSpec.join( sep === undefined ? ', ' : (sep || '') ); ret = fontSpec.join( sep === undefined ? ', ' : (sep || '') );
} } else {
else {
// A font description object. // A font description object.
ret = fontSpec.name; ret = fontSpec.name;
} }
} }
} }
@ -273,8 +322,7 @@ Generic template helper definitions for HackMyResume / FluentCV.
helper: 'fontList', error: HMSTATUS.missingParam, helper: 'fontList', error: HMSTATUS.missingParam,
expected: 'defFontList'}); expected: 'defFontList'});
ret = ''; ret = '';
} } else {
else {
ret = defFontList; ret = defFontList;
} }
} }
@ -282,30 +330,22 @@ Generic template helper definitions for HackMyResume / FluentCV.
return ret; return ret;
}, },
/** /**
Capitalize the first letter of the word. Capitalize the first letter of the word. TODO: Rename
@method section @method section
*/ */
camelCase: function(val) { camelCase(val) {
val = (val && val.trim()) || ''; val = (val && val.trim()) || '';
return val ? (val.charAt(0).toUpperCase() + val.slice(1)) : val; if (val) { return (val.charAt(0).toUpperCase() + val.slice(1)); } else { return val; }
}, },
/**
Return true if the context has the property or subpropery.
@method has
*/
has: function( title, options ) {
title = title && title.trim().toLowerCase();
if( LO.get( this.r, title ) ) {
return options.fn(this);
}
},
/** /**
Generic template helper function to display a user-overridable section Display a user-overridable section title for a FRESH resume theme. Use this in
title for a FRESH resume theme. Use this in lieue of hard-coding section lieue of hard-coding section titles.
titles.
Usage: Usage:
@ -323,11 +363,12 @@ Generic template helper definitions for HackMyResume / FluentCV.
user. user.
@method sectionTitle @method sectionTitle
*/ */
sectionTitle: function( sname, stitle ) { sectionTitle( sname, stitle ) {
// If not provided by the user, stitle should default to sname. ps. // If not provided by the user, stitle should default to sname. ps.
// Handlebars silently passes in the options object to the last param, // Handlebars silently passes in the options object to the last param,
// where in Underscore stitle will be null/undefined, so we check both. // where in Underscore stitle will be null/undefined, so we check both.
// TODO: not actually sure that's true, given that we _.wrap these functions
stitle = (stitle && String.is(stitle) && stitle) || sname; stitle = (stitle && String.is(stitle) && stitle) || sname;
// If there's a section title override, use it. // If there's a section title override, use it.
@ -336,38 +377,50 @@ Generic template helper definitions for HackMyResume / FluentCV.
stitle; stitle;
}, },
/**
Convert inline Markdown to inline WordProcessingML.
@method wpml /** Convert inline Markdown to inline WordProcessingML. */
*/ wpml( txt, inline ) {
wpml: function( txt, inline ) { if (!txt) { return ''; }
if(!txt) return '';
inline = (inline && !inline.hash) || false; inline = (inline && !inline.hash) || false;
txt = XML(txt.trim()); txt = XML(txt.trim());
txt = inline ? txt = inline ? MD(txt).replace(/^\s*<p>|<\/p>\s*$/gi, '') : MD(txt);
MD(txt).replace(/^\s*<p>|<\/p>\s*$/gi, '') :
MD(txt);
txt = H2W( txt ); txt = H2W( txt );
return txt; return txt;
}, },
/** /**
Emit a conditional link. Emit a conditional link.
@method link @method link
*/ */
link: function( text, url ) { link( text, url ) {
return url && url.trim() ? if (url && url.trim()) { return (`<a href="${url}">${text}</a>`); } else { return text; }
('<a href="' + url + '">' + text + '</a>') : text;
}, },
/**
Emit a conditional Markdown link.
@method link
*/
linkMD( text, url ) {
if (url && url.trim()) { return (`[${text}](${url})`); } else { return text; }
},
/** /**
Return the last word of the specified text. Return the last word of the specified text.
@method lastWord @method lastWord
*/ */
lastWord: function( txt ) { lastWord( txt ) {
return txt && txt.trim() ? _.last( txt.split(' ') ) : ''; if (txt && txt.trim()) { return _.last( txt.split(' ') ); } else { return ''; }
}, },
/** /**
Convert a skill level to an RGB color triplet. TODO: refactor Convert a skill level to an RGB color triplet. TODO: refactor
@method skillColor @method skillColor
@ -376,62 +429,62 @@ Generic template helper definitions for HackMyResume / FluentCV.
integer ("1", "5", etc.), or as an RRGGBB color triplet ('#C00000', integer ("1", "5", etc.), or as an RRGGBB color triplet ('#C00000',
'#FFFFAA'). '#FFFFAA').
*/ */
skillColor: function( lvl ) { skillColor( lvl ) {
var idx = skillLevelToIndex( lvl ); const idx = _skillLevelToIndex(lvl);
var skillColors = (this.theme && this.theme.palette && const skillColors = (this.theme && this.theme.palette &&
this.theme.palette.skillLevels) || this.theme.palette.skillLevels) ||
[ '#FFFFFF', '#5CB85C', '#F1C40F', '#428BCA', '#C00000' ]; [ '#FFFFFF', '#5CB85C', '#F1C40F', '#428BCA', '#C00000' ];
return skillColors[idx]; return skillColors[idx];
}, },
/** /**
Return an appropriate height. TODO: refactor Return an appropriate height. TODO: refactor
@method lastWord @method lastWord
*/ */
skillHeight: function( lvl ) { skillHeight( lvl ) {
var idx = skillLevelToIndex( lvl ); const idx = _skillLevelToIndex(lvl);
return ['38.25', '30', '16', '8', '0'][idx]; return ['38.25', '30', '16', '8', '0'][idx];
}, },
/** /**
Return all but the last word of the input text. Return all but the last word of the input text.
@method initialWords @method initialWords
*/ */
initialWords: function( txt ) { initialWords( txt ) {
return txt && txt.trim() ? _.initial( txt.split(' ') ).join(' ') : ''; if (txt && txt.trim()) { return _.initial( txt.split(' ') ).join(' '); } else { return ''; }
}, },
/** /**
Trim the protocol (http or https) from a URL/ Trim the protocol (http or https) from a URL/
@method trimURL @method trimURL
*/ */
trimURL: function( url ) { trimURL( url ) {
return url && url.trim() ? url.trim().replace(/^https?:\/\//i, '') : ''; if (url && url.trim()) { return url.trim().replace(/^https?:\/\//i, ''); } else { return ''; }
}, },
/** /**
Convert text to lowercase. Convert text to lowercase.
@method toLower @method toLower
*/ */
toLower: function( txt ) { toLower( txt ) { if (txt && txt.trim()) { return txt.toLowerCase(); } else { return ''; } },
return txt && txt.trim() ? txt.toLowerCase() : '';
},
/** /**
Convert text to lowercase. Convert text to lowercase.
@method toLower @method toLower
*/ */
toUpper: function( txt ) { toUpper( txt ) { if (txt && txt.trim()) { return txt.toUpperCase(); } else { return ''; } },
return txt && txt.trim() ? txt.toUpperCase() : '';
},
/**
Return true if either value is truthy.
@method either
*/
either: function( lhs, rhs, options ) {
if (lhs || rhs) return options.fn(this);
},
/** /**
Conditional stylesheet link. Creates a link to the specified stylesheet with Conditional stylesheet link. Creates a link to the specified stylesheet with
@ -442,32 +495,28 @@ Generic template helper definitions for HackMyResume / FluentCV.
`link`. If omitted, defaults to `embed`. Can be overridden by the `--css` `link`. If omitted, defaults to `embed`. Can be overridden by the `--css`
command-line switch. command-line switch.
*/ */
styleSheet( url, linkage ) {
styleSheet: function( url, linkage ) {
// Establish the linkage style // Establish the linkage style
linkage = this.opts.css || linkage || 'embed'; linkage = this.opts.css || linkage || 'embed';
// Create the <link> or <style> tag // Create the <link> or <style> tag
var ret = ''; let ret = '';
if (linkage === 'link') { if (linkage === 'link') {
ret = printf('<link href="%s" rel="stylesheet" type="text/css">', url); ret = printf('<link href="%s" rel="stylesheet" type="text/css">', url);
} } else {
else { const rawCss = FS.readFileSync(
var rawCss = FS.readFileSync(
PATH.join( this.opts.themeObj.folder, '/src/', url ), 'utf8' ); PATH.join( this.opts.themeObj.folder, '/src/', url ), 'utf8' );
var renderedCss = this.engine.generateSimple( this, rawCss ); const renderedCss = this.engine.generateSimple( this, rawCss );
ret = printf('<style>%s</style>', renderedCss ); ret = printf('<style>%s</style>', renderedCss );
} }
// If the currently-executing template is inherited, append styles // If the currently-executing template is inherited, append styles
if( this.opts.themeObj.inherits && if (this.opts.themeObj.inherits && this.opts.themeObj.inherits.html && (this.format === 'html')) {
this.opts.themeObj.inherits.html && ret +=
this.format === 'html' ) { (linkage === 'link')
ret += (linkage === 'link') ? ? `<link href="${this.opts.themeObj.overrides.path}" rel="stylesheet" type="text/css">`
'<link href="' + this.opts.themeObj.overrides.path + : `<style>${this.opts.themeObj.overrides.data}</style>`;
'" rel="stylesheet" type="text/css">' :
'<style>' + this.opts.themeObj.overrides.data + '</style>';
} }
// TODO: It would be nice to use Handlebar.SafeString here, but these // TODO: It would be nice to use Handlebar.SafeString here, but these
@ -476,47 +525,91 @@ Generic template helper definitions for HackMyResume / FluentCV.
return ret; return ret;
}, },
/** /**
Perform a generic comparison. Perform a generic comparison.
See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates
@method compare @method compare
*/ */
compare: function(lvalue, rvalue, options) { compare(lvalue, rvalue, options) {
if (arguments.length < 3) if (arguments.length < 3) {
throw new Error("Handlerbars Helper 'compare' needs 2 parameters"); throw new Error('Template 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);
} }
const operator = options.hash.operator || '==';
const operators = {
'=='(l,r) { return l === r; },
'==='(l,r) { return l === r; },
'!='(l,r) { return l !== r; },
'<'(l,r) { return l < r; },
'>'(l,r) { return l > r; },
'<='(l,r) { return l <= r; },
'>='(l,r) { return l >= r; },
'typeof'(l,r) { return typeof l === r; }
}; };
if (!operators[operator]) {
throw new Error(`Helper 'compare' doesn't know the operator ${operator}`);
}
const result = operators[operator]( lvalue, rvalue );
if (result) { return options.fn(this); } else { return options.inverse(this); }
},
/**
Emit padded text.
*/
pad(stringOrArray, padAmount/*, unused*/ ) {
stringOrArray = stringOrArray || '';
padAmount = padAmount || 0;
let ret = '';
const PAD = require('string-padding');
if (!String.is(stringOrArray)) {
ret = stringOrArray
.map(line => PAD(line, line.length + Math.abs(padAmount), null, padAmount < 0 ? PAD.LEFT : PAD.RIGHT))
.join('\n');
} else {
ret = PAD(stringOrArray, stringOrArray.length + Math.abs(padAmount), null, padAmount < 0 ? PAD.LEFT : PAD.RIGHT);
}
return ret;
},
/**
Given the name of a skill ("JavaScript" or "HVAC repair"), return the number
of years assigned to that skill in the resume.skills.list collection.
*/
skillYears( skill, rez ) {
const sk = _.find(rez.skills.list, sk => sk.name.toUpperCase() === skill.toUpperCase());
if (sk) { return sk.years; } else { return '?'; }
},
/**
Given an object that may be a string or an object, return it as-is if it's a
string, otherwise return the value at obj[objPath].
*/
stringOrObject( obj, objPath/*, rez */) {
if (_.isString(obj)) { return obj; } else { return LO.get(obj, objPath); }
}
});
/** /**
Report an error to the outside world without throwing an exception. Currently Report an error to the outside world without throwing an exception. Currently
relies on kludging the running verb into. opts. relies on kludging the running verb into. opts.
*/ */
function _reportError( code, params ) { var _reportError = ( code, params ) => GenericHelpers.opts.errHandler.err( code, params );
GenericHelpers.opts.errHandler.err( code, params );
}
/** /**
Format a from/to date range for display. Format a from/to date range for display.
*/ */
function _fromTo( dateA, dateB, fmt, sep, fallback ) { var _fromTo = function( dateA, dateB, fmt, sep, fallback ) {
// Prevent accidental use of safe.start, safe.end, safe.date // Prevent accidental use of safe.start, safe.end, safe.date
// The dateRange helper is for raw dates only // The dateRange helper is for raw dates only
if (moment.isMoment( dateA ) || moment.isMoment( dateB )) { if (moment.isMoment( dateA ) || moment.isMoment( dateB )) {
@ -524,49 +617,52 @@ Generic template helper definitions for HackMyResume / FluentCV.
return ''; return '';
} }
var dateFrom, dateTo, dateTemp; let dateFrom = null;
let dateTo = null;
let dateTemp = null;
// Check for 'current', 'present', 'now', '', null, and undefined // Check for 'current', 'present', 'now', '', null, and undefined
dateA = dateA || ''; dateA = dateA || '';
dateB = dateB || ''; dateB = dateB || '';
var dateATrim = dateA.trim().toLowerCase(); const dateATrim = dateA.trim().toLowerCase();
var dateBTrim = dateB.trim().toLowerCase(); const dateBTrim = dateB.trim().toLowerCase();
var reserved = ['current','present','now', '']; const reserved = ['current','present','now', ''];
fmt = (fmt && String.is(fmt) && fmt) || 'YYYY-MM'; fmt = (fmt && String.is(fmt) && fmt) || 'YYYY-MM';
sep = (sep && String.is(sep) && sep) || ' — '; sep = (sep && String.is(sep) && sep) || ' — ';
if (_.contains( reserved, dateATrim )) { if (_.contains( reserved, dateATrim )) {
dateFrom = fallback || '???'; dateFrom = fallback || '???';
} } else {
else {
dateTemp = FluentDate.fmt( dateA ); dateTemp = FluentDate.fmt( dateA );
dateFrom = dateTemp.format( fmt ); dateFrom = dateTemp.format( fmt );
} }
if (_.contains( reserved, dateBTrim )) { if (_.contains( reserved, dateBTrim )) {
dateTo = fallback || 'Current'; dateTo = fallback || 'Present';
} } else {
else {
dateTemp = FluentDate.fmt( dateB ); dateTemp = FluentDate.fmt( dateB );
dateTo = dateTemp.format( fmt ); dateTo = dateTemp.format( fmt );
} }
if( dateFrom && dateTo ) {
if (dateFrom === dateTo) {
return dateFrom;
} else if (dateFrom && dateTo) {
return dateFrom + sep + dateTo; return dateFrom + sep + dateTo;
} } else if (dateFrom || dateTo) {
else if( dateFrom || dateTo ) {
return dateFrom || dateTo; return dateFrom || dateTo;
} }
return ''; return '';
} };
function skillLevelToIndex( lvl ) {
var idx = 0;
var _skillLevelToIndex = function( lvl ) {
let idx = 0;
if (String.is( lvl )) { if (String.is( lvl )) {
lvl = lvl.trim().toLowerCase(); lvl = lvl.trim().toLowerCase();
var intVal = parseInt( lvl ); const intVal = parseInt( lvl );
if (isNaN(intVal)) { if (isNaN(intVal)) {
switch (lvl) { switch (lvl) {
case 'beginner': idx = 1; break; case 'beginner': idx = 1; break;
@ -574,20 +670,18 @@ Generic template helper definitions for HackMyResume / FluentCV.
case 'advanced': idx = 3; break; case 'advanced': idx = 3; break;
case 'master': idx = 4; break; case 'master': idx = 4; break;
} }
} } else {
else {
idx = Math.min( intVal / 2, 4 ); idx = Math.min( intVal / 2, 4 );
idx = Math.max( 0, idx ); idx = Math.max( 0, idx );
} }
} } else {
else {
idx = Math.min( lvl / 2, 4 ); idx = Math.min( lvl / 2, 4 );
idx = Math.max( 0, idx ); idx = Math.max( 0, idx );
} }
return idx; return idx;
} };
}());
// Note [1] -------------------------------------------------------------------- // Note [1] --------------------------------------------------------------------
// Make sure it's precisely a string or array since some template engines jam // Make sure it's precisely a string or array since some template engines jam

View File

@ -1,26 +1,89 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Template helper definitions for Handlebars. Template helper definitions for Handlebars.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
@module handlebars-helpers.js @module handlebars-helpers.js
*/ */
const HANDLEBARS = require('handlebars');
(function() { const _ = require('underscore');
const helpers = require('./generic-helpers');
var HANDLEBARS = require('handlebars') const path = require('path');
, _ = require('underscore') const blockHelpers = require('./block-helpers');
, helpers = require('./generic-helpers'); const HMS = require('../core/status-codes');
/** /**
Register useful Handlebars helpers. Register useful Handlebars helpers.
@method registerHelpers @method registerHelpers
*/ */
module.exports = function( theme, opts ) {
module.exports = function( theme, rez, opts ) {
helpers.theme = theme; helpers.theme = theme;
helpers.opts = opts; helpers.opts = opts;
HANDLEBARS.registerHelper( helpers ); helpers.type = 'handlebars';
// Prepare generic helpers for use with Handlebars. We do this by wrapping them
// in a Handlebars-aware wrapper which calls the helper internally.
const wrappedHelpers = _.mapObject(helpers, function( hVal/*, hKey*/ ) {
if (_.isFunction(hVal)) {
return _.wrap(hVal, function(func) {
const args = Array.prototype.slice.call(arguments);
args.shift(); // lose the 1st element (func) [^1]
//args.pop() # lose the last element (HB options hash)
args[ args.length - 1 ] = rez; // replace w/ resume object
return func.apply(this, args);
}); // call the generic helper
}
return hVal;
}
, this);
HANDLEBARS.registerHelper(wrappedHelpers);
// Prepare Handlebars-specific helpers - "blockHelpers" is really a misnomer
// since any kind of Handlebars-specific helper can live here
HANDLEBARS.registerHelper(blockHelpers);
// Register any theme-provided custom helpers...
// Normalize "theme.helpers" (string or array) to an array
if (_.isString(theme.helpers)) { theme.helpers = [ theme.helpers ]; }
if (_.isArray(theme.helpers)) {
const glob = require('glob');
const slash = require('slash');
let curGlob = null;
try {
_.each(theme.helpers, function(fGlob) { // foreach theme.helpers entry
curGlob = fGlob; // ..cache in case of exception
fGlob = path.join(theme.folder, fGlob); // ..make relative to theme
const files = glob.sync(slash(fGlob)); // ..expand the glob
if (files.length > 0) { // ..guard against empty glob
_.each(files, function(f) { // ..loop over concrete paths
HANDLEBARS.registerHelper(require(f)); // ..register the path
});
} else {
throw {fluenterror: HMS.themeHelperLoad, inner: null, glob: fGlob};
}
});
return;
} catch (ex) {
throw{
fluenterror: HMS.themeHelperLoad,
inner: ex,
glob: curGlob, exit: true
};
}
}
}; };
}()); // [^1]: This little bit of acrobatics ensures that our generic helpers are
// called as generic helpers, not as Handlebars-specific helpers. This allows
// them to be used in other templating engines, like Underscore. If you need a
// Handlebars-specific helper with normal Handlebars context and options, put it
// in block-helpers.coffee.

View File

@ -1,34 +1,35 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Template helper definitions for Underscore. Template helper definitions for Underscore.
@license MIT. Copyright (c) 2016 hacksalot (https://github.com/hacksalot) @license MIT. See LICENSE.md for details.
@module handlebars-helpers.js @module handlebars-helpers.js
*/ */
(function() {
var HANDLEBARS = require('handlebars') const _ = require('underscore');
, _ = require('underscore') const helpers = require('./generic-helpers');
, helpers = require('./generic-helpers');
/** /**
Register useful Underscore helpers. Register useful Underscore helpers.
@method registerHelpers @method registerHelpers
*/ */
module.exports = function( theme, opts, cssInfo, ctx, eng ) { module.exports = function( theme, opts, cssInfo, ctx, eng ) {
helpers.theme = theme; helpers.theme = theme;
helpers.opts = opts; helpers.opts = opts;
helpers.cssInfo = cssInfo; helpers.cssInfo = cssInfo;
helpers.engine = eng; helpers.engine = eng;
ctx.h = helpers; ctx.h = helpers;
_.each(helpers, function( hVal ) {
_.each( helpers, function( hVal, hKey ) {
if (_.isFunction(hVal)) { if (_.isFunction(hVal)) {
_.bind( hVal, ctx ); return _.bind(hVal, ctx);
} }
}, this); }
, this);
}; };
}());

View File

@ -1,22 +1,46 @@
#! /usr/bin/env node
/** /**
Command-line interface (CLI) for HackMyResume. External API surface for HackMyResume.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@module index.js @module hackmycore/index
*/ */
try { /** API facade for HackMyResume. */
require('./cli/main')( process.argv ); module.exports = {
} verbs: {
catch( ex ) { build: require('./verbs/build'),
analyze: require('./verbs/analyze'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
new: require('./verbs/create'),
peek: require('./verbs/peek')
},
require('./cli/error').err( ex, true ); alias: {
generate: require('./verbs/build'),
create: require('./verbs/create')
},
} options: require('./core/default-options'),
formats: require('./core/default-formats'),
Sheet: require('./core/fresh-resume'),
FRESHResume: require('./core/fresh-resume'),
JRSResume: require('./core/jrs-resume'),
FRESHTheme: require('./core/fresh-theme'),
JRSTheme: require('./core/jrs-theme'),
ResumeFactory: require('./core/resume-factory'),
FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./generators/html-generator'),
TextGenerator: require('./generators/text-generator'),
HtmlPdfCliGenerator: require('./generators/html-pdf-cli-generator'),
WordGenerator: require('./generators/word-generator'),
MarkdownGenerator: require('./generators/markdown-generator'),
JsonGenerator: require('./generators/json-generator'),
YamlGenerator: require('./generators/yaml-generator'),
JsonYamlGenerator: require('./generators/json-yaml-generator'),
LaTeXGenerator: require('./generators/latex-generator'),
HtmlPngGenerator: require('./generators/html-png-generator')
};

View File

@ -0,0 +1,54 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const FluentDate = require('../core/fluent-date');
const _ = require('underscore');
const lo = require('lodash');
module.exports = {
/**
Compute the total duration of the work history.
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
run(rez, collKey, startKey, endKey, unit) {
unit = unit || 'years';
const hist = lo.get(rez, collKey);
if (!hist || !hist.length) { return 0; }
// BEGIN CODE DUPLICATION --> src/inspectors/gap-inspector.coffee (TODO)
// Convert the candidate's employment history to an array of dates,
// where each element in the array is a start date or an end date of a
// job -- it doesn't matter which.
let new_e = hist.map(function( job ) {
let obj = _.pick( job, [startKey, endKey] );
// Synthesize an end date if this is a "current" gig
if (!_.has(obj, endKey)) { obj[endKey] = 'current'; }
if (obj && (obj[startKey] || obj[endKey])) {
obj = _.pairs(obj);
obj[0][1] = FluentDate.fmt( obj[0][1] );
if (obj.length > 1) {
obj[1][1] = FluentDate.fmt( obj[1][1] );
}
}
return obj;
});
// Flatten the array, remove empties, and sort
new_e = _.filter(_.flatten( new_e, true ), v => v && v.length && v[0] && v[0].length);
if (!new_e || !new_e.length) { return 0; }
new_e = _.sortBy(new_e, elem => elem[1].unix());
// END CODE DUPLICATION
const firstDate = _.first( new_e )[1];
const lastDate = _.last( new_e )[1];
return lastDate.diff(firstDate, unit);
}
};

View File

@ -1,3 +1,8 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Employment gap analysis for HackMyResume. Employment gap analysis for HackMyResume.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@ -6,28 +11,20 @@ Employment gap analysis for HackMyResume.
(function() { const _ = require('underscore');
const FluentDate = require('../core/fluent-date');
const moment = require('moment');
const LO = require('lodash');
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. Identify gaps in the candidate's employment history.
*/ */
var gapInspector = module.exports = { module.exports = {
moniker: 'gap-inspector', moniker: 'gap-inspector',
/** /**
Run the Gap Analyzer on a resume. Run the Gap Analyzer on a resume.
@method run @method run
@ -40,45 +37,46 @@ Employment gap analysis for HackMyResume.
duration: // Gap length duration: // Gap length
} }
*/ */
run: function( rez ) {
run(rez) {
// This is what we'll return // This is what we'll return
var coverage = { const coverage = {
gaps: [], gaps: [],
overlaps: [], overlaps: [],
pct: '0%',
duration: { duration: {
total: 0, total: 0,
work: 0, work: 0,
gaps: 0 gaps: 0
}, }
pct: '0%'
}; };
// Missing employment section? Bye bye. // Missing employment section? Bye bye.
var hist = LO.get( rez, 'employment.history' ); const hist = LO.get(rez, 'employment.history');
if (!hist || !hist.length) { return coverage; } if (!hist || !hist.length) { return coverage; }
// Convert the candidate's employment history to an array of dates, // 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 // where each element in the array is a start date or an end date of a
// job -- it doesn't matter which. // job -- it doesn't matter which.
var new_e = hist.map( function( job ){ let new_e = hist.map( function( job ) {
var obj = _.pick( job, ['start', 'end'] ); let obj = _.pick( job, ['start', 'end'] );
if (obj && (obj.start || obj.end)) { if (obj && (obj.start || obj.end)) {
obj = _.pairs( obj ); obj = _.pairs( obj );
obj[0][1] = FluentDate.fmt( obj[0][1] ); obj[0][1] = FluentDate.fmt( obj[0][1] );
if( obj.length > 1 ) if (obj.length > 1) {
obj[1][1] = FluentDate.fmt( obj[1][1] ); obj[1][1] = FluentDate.fmt( obj[1][1] );
} }
}
return obj; return obj;
}); });
// Flatten the array, remove empties, and sort // Flatten the array, remove empties, and sort
new_e = _.filter( _.flatten( new_e, true ), function(v) { new_e = _.filter(_.flatten( new_e, true ), v => v && v.length && v[0] && v[0].length);
return ( v && v.length && v[0] && v[0].length );
}); if (!new_e || !new_e.length) { return coverage; }
if( !new_e || !new_e.length ) return coverage; new_e = _.sortBy(new_e, elem => elem[1].unix());
new_e = _.sortBy( new_e, function( elem ) { return elem[1].unix(); });
// Iterate over elements in the array. Each time a start date is found, // 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 // increment a reference count. Each time an end date is found, decrement
@ -86,58 +84,58 @@ Employment gap analysis for HackMyResume.
// When the reference count is > 0, the candidate is employed. When the // When the reference count is > 0, the candidate is employed. When the
// reference count reaches 2, the candidate is overlapped. // reference count reaches 2, the candidate is overlapped.
var num_gaps = 0, ref_count = 0, total_gap_days = 0, gap_start; let ref_count = 0;
let total_gap_days = 0;
new_e.forEach(function(point) { new_e.forEach(function(point) {
var inc = point[0] === 'start' ? 1 : -1; const inc = point[0] === 'start' ? 1 : -1;
ref_count += inc; ref_count += inc;
// If the ref count just reached 0, start a new GAP // If the ref count just reached 0, start a new GAP
if (ref_count === 0) { if (ref_count === 0) {
coverage.gaps.push( { start: point[1], end: null }); return coverage.gaps.push( { start: point[1], end: null });
}
// If the ref count reached 1 by rising, end the last GAP // If the ref count reached 1 by rising, end the last GAP
else if( ref_count === 1 && inc === 1 ) { } else if ((ref_count === 1) && (inc === 1)) {
var lastGap = _.last( coverage.gaps ); const lastGap = _.last( coverage.gaps );
if (lastGap) { if (lastGap) {
lastGap.end = point[1]; lastGap.end = point[1];
lastGap.duration = lastGap.end.diff( lastGap.start, 'days' ); lastGap.duration = lastGap.end.diff( lastGap.start, 'days' );
total_gap_days += lastGap.duration; return total_gap_days += lastGap.duration;
}
} }
// If the ref count reaches 2 by rising, start a new OVERLAP // If the ref count reaches 2 by rising, start a new OVERLAP
else if( ref_count === 2 && inc === 1 ) { } else if ((ref_count === 2) && (inc === 1)) {
coverage.overlaps.push( { start: point[1], end: null }); return coverage.overlaps.push( { start: point[1], end: null });
}
// If the ref count reaches 1 by falling, end the last OVERLAP // If the ref count reaches 1 by falling, end the last OVERLAP
else if( ref_count === 1 && inc === -1 ) { } else if ((ref_count === 1) && (inc === -1)) {
var lastOver = _.last( coverage.overlaps ); const lastOver = _.last( coverage.overlaps );
if (lastOver) { if (lastOver) {
lastOver.end = point[1]; lastOver.end = point[1];
lastOver.duration = lastOver.end.diff( lastOver.start, 'days' ); lastOver.duration = lastOver.end.diff( lastOver.start, 'days' );
if (lastOver.duration === 0) { if (lastOver.duration === 0) {
coverage.overlaps.pop(); return coverage.overlaps.pop();
} }
} }
} }
}); });
// It's possible that the last gap/overlap didn't have an explicit .end // It's possible that the last gap/overlap didn't have an explicit .end
// date.If so, set the end date to the present date and compute the // date.If so, set the end date to the present date and compute the
// duration normally. // duration normally.
if (coverage.overlaps.length) { if (coverage.overlaps.length) {
var o = _.last( coverage.overlaps ); const o = _.last( coverage.overlaps );
if (o && !o.end) { if (o && !o.end) {
o.end = moment(); o.end = moment();
o.duration = o.end.diff( o.start, 'days' ); o.duration = o.end.diff( o.start, 'days' );
} }
} }
if (coverage.gaps.length) { if (coverage.gaps.length) {
var g = _.last( coverage.gaps ); const g = _.last( coverage.gaps );
if (g && !g.end) { if (g && !g.end) {
g.end = moment(); g.end = moment();
g.duration = g.end.diff( g.start, 'days' ); g.duration = g.end.diff( g.start, 'days' );
@ -145,22 +143,15 @@ Employment gap analysis for HackMyResume.
} }
// Package data for return to the client // Package data for return to the client
var tdur = rez.duration('days'); const tdur = rez.duration('days');
var dur = { const dur = {
total: tdur, total: tdur,
work: tdur - total_gap_days, work: tdur - total_gap_days,
gaps: total_gap_days gaps: total_gap_days
}; };
coverage.pct = ( dur.total > 0 && dur.work > 0 ) ?
((((dur.total - dur.gaps) / dur.total) * 100)).toFixed(1) + '%' : coverage.pct = (dur.total > 0) && (dur.work > 0) ? ((((dur.total - dur.gaps) / dur.total) * 100)).toFixed(1) + '%' : '???';
'???';
coverage.duration = dur; coverage.duration = dur;
return coverage; return coverage;
} }
}; };
}());

View File

@ -1,50 +1,36 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Keyword analysis for HackMyResume. Keyword analysis for HackMyResume.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@module keyword-inspector.js @module inspectors/keyword-inspector
*/ */
(function() {
var _ = require('underscore');
var FluentDate = require('../core/fluent-date');
/** /**
Analyze the resume's use of keywords. Analyze the resume's use of keywords.
TODO: BUG: Keyword search regex is inaccurate, especially for one or two TODO: BUG: Keyword search regex is inaccurate, especially for one or two
letter keywords like "C" or "CLI". letter keywords like "C" or "CLI".
@class keywordInspector @class keywordInspector
*/ */
var keywordInspector = module.exports = { module.exports = {
/** A unique name for this inspector. */
/**
A unique name for this inspector.
*/
moniker: 'keyword-inspector', moniker: 'keyword-inspector',
/** /**
Run the Keyword Inspector on a resume. Run the Keyword Inspector on a resume.
@method run @method run
@return An collection of statistical keyword data. @return An collection of statistical keyword data.
*/ */
run: function( rez ) { run( rez ) {
// "Quote" or safely escape a keyword so it can be used as a regex. For // "Quote" or safely escape a keyword so it can be used as a regex. For
// example, if the keyword is "C++", yield "C\+\+". // example, if the keyword is "C++", yield "C\+\+".
// http://stackoverflow.com/a/2593661/4942583 // http://stackoverflow.com/a/2593661/4942583
function regex_quote(str) { const regex_quote = str => (str + '').replace(/[.?*+^$[\]\\(){}|-]/ig, '\\$&');
return (str + '').replace(/[.?*+^$[\]\\(){}|-]/ig, "\\$&");
}
// Create a searchable plain-text digest of the resume // Create a searchable plain-text digest of the resume
// TODO: BUG: Don't search within keywords for other keywords. Job A // TODO: BUG: Don't search within keywords for other keywords. Job A
@ -52,15 +38,13 @@ Keyword analysis for HackMyResume.
// B's mention of "foobar" should not count as a mention of "foo". // B's mention of "foobar" should not count as a mention of "foo".
// To achieve this, remove keywords from the search digest and treat them // To achieve this, remove keywords from the search digest and treat them
// separately. // separately.
var searchable = ''; let searchable = '';
rez.transformStrings( ['imp', 'computed', 'safe'], function trxString( key, val ) { rez.transformStrings(['imp', 'computed', 'safe'], ( key, val ) => searchable += ` ${val}`);
searchable += ' ' + val;
});
// Assemble a regex skeleton we can use to test for keywords with a bit // Assemble a regex skeleton we can use to test for keywords with a bit
// more // more
var prefix = '(?:' + ['^', '\\s+', '[\\.,]+'].join('|') + ')'; const prefix = `(?:${['^', '\\s+', '[\\.,]+'].join('|')})`;
var suffix = '(?:' + ['$', '\\s+', '[\\.,]+'].join('|') + ')'; const suffix = `(?:${['$', '\\s+', '[\\.,]+'].join('|')})`;
return rez.keywords().map(function(kw) { return rez.keywords().map(function(kw) {
@ -75,24 +59,16 @@ Keyword analysis for HackMyResume.
// //
// 3. Instead, use a custom regex with special delimeters. // 3. Instead, use a custom regex with special delimeters.
var regex_str = prefix + regex_quote( kw ) + suffix; const regex_str = prefix + regex_quote( kw ) + suffix;
var regex = new RegExp( regex_str, 'ig'); const regex = new RegExp( regex_str, 'ig');
var myArray, count = 0; let count = 0;
while ((myArray = regex.exec( searchable )) !== null) { while (regex.exec( searchable ) !== null) {
count++; count++;
} }
return { return {
name: kw, name: kw,
count: count count
}; };
}); });
} }
}; };
}());

View File

@ -1,50 +1,40 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Section analysis for HackMyResume. Section analysis for HackMyResume.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@module totals-inspector.js @module inspectors/totals-inspector
*/ */
const _ = require('underscore');
(function() {
var _ = require('underscore');
var FluentDate = require('../core/fluent-date');
/** /**
Retrieve sectional overview and summary information. Retrieve sectional overview and summary information.
@class totalsInspector @class totalsInspector
*/ */
var totalsInspector = module.exports = { module.exports = {
moniker: 'totals-inspector', moniker: 'totals-inspector',
/** /**
Run the Totals Inspector on a resume. Run the Totals Inspector on a resume.
@method run @method run
@return An object containing summary information for each section on the @return An object containing summary information for each section on the
resume. resume.
*/ */
run: function( rez ) { run( rez ) {
var sectionTotals = { }; const sectionTotals = { };
_.each(rez, function(val, key) { _.each(rez, function(val, key) {
if (_.isArray( val ) && !_.isString(val)) { if (_.isArray( val ) && !_.isString(val)) {
sectionTotals[ key ] = val.length; return sectionTotals[ key ] = val.length;
} } else if (val.history && _.isArray( val.history )) {
else if( val.history && _.isArray( val.history ) ) { return sectionTotals[ key ] = val.history.length;
sectionTotals[ key ] = val.history.length; } else if (val.sets && _.isArray( val.sets )) {
} return sectionTotals[ key ] = val.sets.length;
else if( val.sets && _.isArray( val.sets ) ) {
sectionTotals[ key ] = val.sets.length;
} }
}); });
@ -52,12 +42,5 @@ Section analysis for HackMyResume.
totals: sectionTotals, totals: sectionTotals,
numSections: Object.keys( sectionTotals ).length numSections: Object.keys( sectionTotals ).length
}; };
} }
}; };
}());

View File

@ -1,3 +1,8 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the HandlebarsGenerator class. Definition of the HandlebarsGenerator class.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@ -5,20 +10,15 @@ Definition of the HandlebarsGenerator class.
*/ */
const _ = require('underscore');
(function() { const HANDLEBARS = require('handlebars');
const FS = require('fs');
const registerHelpers = require('../helpers/handlebars-helpers');
const PATH = require('path');
var _ = require('underscore') const parsePath = require('parse-filepath');
, HANDLEBARS = require('handlebars') const READFILES = require('recursive-readdir-sync');
, FS = require('fs') const HMSTATUS = require('../core/status-codes');
, registerHelpers = require('../helpers/handlebars-helpers') const SLASH = require('slash');
, PATH = require('path')
, parsePath = require('parse-filepath')
, READFILES = require('recursive-readdir-sync')
, HMSTATUS = require('../core/status-codes')
, SLASH = require('slash');
@ -26,47 +26,55 @@ Definition of the HandlebarsGenerator class.
Perform template-based resume generation using Handlebars.js. Perform template-based resume generation using Handlebars.js.
@class HandlebarsGenerator @class HandlebarsGenerator
*/ */
var HandlebarsGenerator = module.exports = { module.exports = {
generateSimple: function( data, tpl ) { generateSimple( data, tpl ) {
let template;
try { try {
// Compile and run the Handlebars template. // Compile and run the Handlebars template.
var template = HANDLEBARS.compile( tpl, { strict: false, assumeObjects: false } ); template = HANDLEBARS.compile(tpl, {
return template( data ); strict: false,
assumeObjects: false,
noEscape: data.opts.noescape
} }
catch( ex ) { );
return template(data);
} catch (err) {
throw{ throw{
fluenterror: template ? fluenterror:
HMSTATUS.invokeTemplate : HMSTATUS.compileTemplate, HMSTATUS[ template ? 'invokeTemplate' : 'compileTemplate' ],
inner: ex inner: err
}; };
} }
}, },
generate: function( json, jst, format, curFmt, opts, theme ) { generate( json, jst, format, curFmt, opts, theme ) {
// Preprocess text
let encData = json;
if ((format === 'html') || (format === 'pdf')) {
encData = json.markdownify();
}
if( format === 'doc' ) {
encData = json.xmlify();
}
// Set up partials and helpers // Set up partials and helpers
registerPartials(format, theme); registerPartials(format, theme);
registerHelpers( theme, opts ); registerHelpers(theme, encData, opts);
// Preprocess text
var encData = json;
( format === 'html' || format === 'pdf' ) && (encData = json.markdownify());
( format === 'doc' ) && (encData = json.xmlify());
// Set up the context // Set up the context
var ctx = { const ctx = {
r: encData, r: encData,
RAW: json, RAW: json,
filt: opts.filters, filt: opts.filters,
format: format, format,
opts: opts, opts,
engine: this, engine: this,
results: curFmt.files, results: curFmt.files,
headFragment: opts.headFragment || '' headFragment: opts.headFragment || ''
@ -75,45 +83,36 @@ Definition of the HandlebarsGenerator class.
// Render the template // Render the template
return this.generateSimple(ctx, jst); return this.generateSimple(ctx, jst);
} }
}; };
function registerPartials(format, theme) { var registerPartials = function(format, theme) {
if( _.contains( ['html','doc','md','txt'], format )) {
if (_.contains( ['html','doc','md','txt','pdf'], format )) {
// Locate the global partials folder // Locate the global partials folder
var partialsFolder = PATH.join( const partialsFolder = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname, parsePath( require.resolve('fresh-themes') ).dirname,
'/partials/', '/partials/',
format format === 'pdf' ? 'html' : format
); );
// Register global partials in the /partials/[format] folder // Register global partials in the /partials/[format] folder
// TODO: Only do this once per HMR invocation. // TODO: Only do this once per HMR invocation.
_.each( READFILES( partialsFolder, function(error){ }), function( el ) { _.each(READFILES( partialsFolder ), function( el ) {
var pathInfo = parsePath( el ); const name = SLASH(PATH.relative( partialsFolder, el ).replace(/\.(?:html|xml|hbs|md|txt)$/i, ''));
var name = SLASH( PATH.relative( partialsFolder, el ) const tplData = FS.readFileSync(el, 'utf8');
.replace(/\.(?:html|xml|hbs|md|txt)$/i, '') ); const compiledTemplate = HANDLEBARS.compile(tplData);
var tplData = FS.readFileSync( el, 'utf8' );
var compiledTemplate = HANDLEBARS.compile( tplData );
HANDLEBARS.registerPartial(name, compiledTemplate); HANDLEBARS.registerPartial(name, compiledTemplate);
theme.partialsInitialized = true; return theme.partialsInitialized = true;
}); });
} }
// Register theme-specific partials // Register theme-specific partials
_.each( theme.partials, function( el ) { return _.each(theme.partials, function( el ) {
var tplData = FS.readFileSync( el.path, 'utf8' ); const tplData = FS.readFileSync(el.path, 'utf8');
var compiledTemplate = HANDLEBARS.compile( tplData ); const compiledTemplate = HANDLEBARS.compile(tplData);
HANDLEBARS.registerPartial( el.name, compiledTemplate ); return HANDLEBARS.registerPartial(el.name, compiledTemplate);
}); });
} };
}());

View File

@ -1,65 +1,45 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the JRSGenerator class. Definition of the JRSGenerator class.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@module jrs-generator.js @module renderers/jrs-generator
*/ */
const MD = require('marked');
(function() {
var _ = require('underscore')
, HANDLEBARS = require('handlebars')
, FS = require('fs')
, registerHelpers = require('../helpers/handlebars-helpers')
, PATH = require('path')
, parsePath = require('parse-filepath')
, READFILES = require('recursive-readdir-sync')
, SLASH = require('slash')
, MD = require('marked');
/** /**
Perform template-based resume generation for JSON Resume themes. Perform template-based resume generation for JSON Resume themes.
@class JRSGenerator @class JRSGenerator
*/ */
var JRSGenerator = module.exports = {
module.exports = {
generate( json, jst, format, cssInfo, opts, theme ) {
generate: function( json, jst, format, cssInfo, opts, theme ) {
// Disable JRS theme chatter (console.log, console.error, etc.) // Disable JRS theme chatter (console.log, console.error, etc.)
var off = ['log', 'error', 'dir'], org = off.map(function(c){ const turnoff = ['log', 'error', 'dir'];
var ret = console[c]; console[c] = function(){}; return ret; const org = turnoff.map(function(c) {
const ret = console[c]; // eslint-disable-line no-console
console[c] = function() {}; // eslint-disable-line no-console
return ret;
}); });
// Freeze and render // Freeze and render
var rezHtml = theme.render( json.harden() ); let rezHtml = theme.render(json.harden());
// Turn logging back on // Turn logging back on
off.forEach(function(c, idx){ console[c] = org[idx]; }); turnoff.forEach((c, idx) => console[c] = org[idx]); // eslint-disable-line no-console
// Unfreeze and apply Markdown // Unfreeze and apply Markdown
rezHtml = rezHtml.replace( /@@@@~.*?~@@@@/gm, function(val){ return rezHtml = rezHtml.replace(/@@@@~[\s\S]*?~@@@@/g, val => MDIN( val.replace( /~@@@@/g,'' ).replace( /@@@@~/g,'' ) ));
return MDIN( val.replace( /~@@@@/gm,'' ).replace( /@@@@~/gm,'' ) );
});
return rezHtml;
} }
}; };
var MDIN = txt => // TODO: Move this
function MDIN(txt) { // TODO: Move this MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '')
return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); ;
}
}());

View File

@ -1,73 +1,90 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the UnderscoreGenerator class. Definition of the UnderscoreGenerator class.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. See LICENSE.md for details.
@module underscore-generator.js @module underscore-generator.js
*/ */
(function() {
const _ = require('underscore');
var _ = require('underscore') const registerHelpers = require('../helpers/underscore-helpers');
, registerHelpers = require('../helpers/underscore-helpers') require('../utils/string');
, HMSTATUS = require('../core/status-codes'); const escapeLaTeX = require('escape-latex');
/** /**
Perform template-based resume generation using Underscore.js. Perform template-based resume generation using Underscore.js.
@class UnderscoreGenerator @class UnderscoreGenerator
*/ */
var UnderscoreGenerator = module.exports = { module.exports = {
generateSimple: function( data, tpl ) {
generateSimple( data, tpl ) {
let t;
try { try {
// Compile and run the Handlebars template. // Compile and run the Handlebars template.
var template = _.template( tpl ); t = _.template(tpl);
return template( data ); return t(data);
} } catch (err) {
catch( ex ) { //console.dir _error
const HMS = require('../core/status-codes');
throw{ throw{
fluenterror: template ? fluenterror: HMS[t ? 'invokeTemplate' : 'compileTemplate'],
HMSTATUS.invokeTemplate : HMSTATUS.compileTemplate, inner: err
inner: ex
}; };
} }
}, },
generate: function( json, jst, format, cssInfo, opts, theme ) {
generate( json, jst, format, cssInfo, opts, theme ) {
// Tweak underscore's default template delimeters // Tweak underscore's default template delimeters
var delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template; let delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template;
if (opts.themeObj && opts.themeObj.delimeters) { if (opts.themeObj && opts.themeObj.delimeters) {
delims = _.mapObject( delims, function(val,key) { delims = _.mapObject(delims, (val) => new RegExp(val, 'ig'));
return new RegExp( val, "ig");
});
} }
_.templateSettings = delims; _.templateSettings = delims;
// Strip {# comments #} // Massage resume strings / text
jst = jst.replace( delims.comment, ''); let r = null;
switch (format) {
case 'html': r = json.markdownify(); break;
case 'pdf': r = json.markdownify(); break;
case 'png': r = json.markdownify(); break;
case 'latex':
var traverse = require('traverse');
r = traverse(json).map(function() {
if (this.isLeaf && String.is(this.node)) {
return escapeLaTeX(this.node);
}
return this.node;
});
break;
default: r = json;
}
var ctx = { // Set up the context
r: format === 'html' || format === 'pdf' || format === 'png' ? json.markdownify() : json, const ctx = {
r,
filt: opts.filters, filt: opts.filters,
XML: require('xml-escape'), XML: require('xml-escape'),
RAW: json, RAW: json,
cssInfo: cssInfo, cssInfo,
//engine: this, //engine: @
headFragment: opts.headFragment || '', headFragment: opts.headFragment || '',
opts: opts opts
}; };
// Link to our helpers
registerHelpers(theme, opts, cssInfo, ctx, this); registerHelpers(theme, opts, cssInfo, ctx, this);
// Generate!
return this.generateSimple(ctx, jst); return this.generateSimple(ctx, jst);
} }
}; };
}());

View File

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

View File

@ -1,12 +1,11 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the SyntaxErrorEx class. Definition of the SyntaxErrorEx class.
@module file-contains.js @module file-contains.js
*/ */
(function(){ module.exports = ( file, needle ) => require('fs').readFileSync(file,'utf-8').indexOf( needle ) > -1;
module.exports = function( file, needle ) {
return require('fs').readFileSync(file,'utf-8').indexOf( needle ) > -1;
};
}());

View File

@ -0,0 +1,27 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Defines a regex suitable for matching FRESH versions.
@module file-contains.js
*/
// Set up a regex that matches all of the following:
//
// - FRESH
// - JRS
// - FRESCA
// - FRESH@1.0.0
// - FRESH@1.0
// - FRESH@1
// - JRS@0.16.0
// - JRS@0.16
// - JRS@0
//
// Don't use a SEMVER regex (eg, NPM's semver-regex) because a) we want to
// support partial semvers like "0" or "1.2" and b) we'll expand this later to
// support fully scoped FRESH versions.
module.exports = () => RegExp('^(FRESH|FRESCA|JRS)(?:@(\\d+(?:\\.\\d+)?(?:\\.\\d+)?))?$');

View File

@ -1,20 +1,25 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the Markdown to WordProcessingML conversion routine. Definition of the Markdown to WordProcessingML conversion routine.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk. @license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module html-to-wpml.js @module utils/html-to-wpml
*/ */
(function(){
var _ = require('underscore'); const XML = require('xml-escape');
var HTML5Tokenizer = require('simple-html-tokenizer'); const _ = require('underscore');
const HTML5Tokenizer = require('simple-html-tokenizer');
module.exports = function( html ) { module.exports = function( html ) {
// Tokenize the HTML stream. // Tokenize the HTML stream.
var tokens = HTML5Tokenizer.tokenize( html ); let is_bold, is_italic, is_link, link_url;
const tokens = HTML5Tokenizer.tokenize( html );
var final = '', is_bold, is_italic, is_link, link_url; let final = (is_bold = (is_italic = (is_link = (link_url = ''))));
// Process <em>, <strong>, and <a> elements in the HTML stream, producing // Process <em>, <strong>, and <a> elements in the HTML stream, producing
// equivalent WordProcessingML that can be dumped into a <w:p> or other // equivalent WordProcessingML that can be dumped into a <w:p> or other
@ -25,42 +30,36 @@ Definition of the Markdown to WordProcessingML conversion routine.
case 'StartTag': case 'StartTag':
switch (tok.tagName) { switch (tok.tagName) {
case 'p': final += '<w:p>'; break; case 'p': return final += '<w:p>';
case 'strong': is_bold = true; break; case 'strong': return is_bold = true;
case 'em': is_italic = true; break; case 'em': return is_italic = true;
case 'a': case 'a':
is_link = true; is_link = true;
link_url = tok.attributes.filter(function(attr){ return link_url = tok.attributes.filter(attr => attr[0] === 'href')[0][1];
return attr[0] === 'href'; }
)[0][1];
break;
} }
break; break;
case 'EndTag': case 'EndTag':
switch (tok.tagName) { switch (tok.tagName) {
case 'p': final += '</w:p>'; break; case 'p': return final += '</w:p>';
case 'strong': is_bold = false; break; case 'strong': return is_bold = false;
case 'em': is_italic = false; break; case 'em': return is_italic = false;
case 'a': is_link = false; break; case 'a': return is_link = false;
} }
break; break;
case 'Chars': case 'Chars':
if( tok.chars.trim().length ) { if( tok.chars.trim().length ) {
var style = is_bold ? '<w:b/>' : ''; let style = is_bold ? '<w:b/>' : '';
style += is_italic ? '<w:i/>' : ''; style += is_italic ? '<w:i/>' : '';
style += is_link ? '<w:rStyle w:val="Hyperlink"/>' : ''; style += is_link ? '<w:rStyle w:val="Hyperlink"/>' : '';
final += return final +=
(is_link ? ('<w:hlink w:dest="' + link_url + '">') : '') + (is_link ? (`<w:hlink w:dest="${link_url}">`) : '') +
'<w:r><w:rPr>' + style + '</w:rPr><w:t>' + tok.chars + '<w:r><w:rPr>' + style + '</w:rPr><w:t>' + XML(tok.chars) +
'</w:t></w:r>' + (is_link ? '</w:hlink>' : ''); '</w:t></w:r>' + (is_link ? '</w:hlink>' : '');
} }
break; break;
} }
}); });
return final; return final;
}; };
}());

View File

@ -1,19 +1,20 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Inline Markdown-to-Chalk conversion routines. Inline Markdown-to-Chalk conversion routines.
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
@module md2chalk.js @module utils/md2chalk
*/ */
(function(){
var MD = require('marked'); const CHALK = require('chalk');
var CHALK = require('chalk'); const LO = require('lodash');
var LO = require('lodash');
module.exports = function( v, style, boldStyle ) { module.exports = function( v, style, boldStyle ) {
boldStyle = boldStyle || 'bold'; boldStyle = boldStyle || 'bold';
var temp = v.replace(/\*\*(.*?)\*\*/g, LO.get( CHALK, boldStyle )('$1')); const temp = v.replace(/\*\*(.*?)\*\*/g, LO.get( CHALK, boldStyle )('$1'));
return style ? LO.get( CHALK, style )(temp) : temp; if (style) { return LO.get( CHALK, style )(temp); } else { return temp; }
}; };
}());

View File

@ -1,12 +1,16 @@
/* eslint-disable */
(function() {
// Exemplar script for generating documents with Phantom.js. // Exemplar script for generating documents with Phantom.js.
// https://raw.githubusercontent.com/ariya/phantomjs/master/examples/rasterize.js // https://raw.githubusercontent.com/ariya/phantomjs/master/examples/rasterize.js
// Converted to CoffeeScript by hacksalot
(function() {
"use strict"; "use strict";
var page = require('webpage').create(), var address, output, page, pageHeight, pageWidth, size, system;
system = require('system'),
address, output, size; page = require('webpage').create();
system = require('system');
address = output = size = null;
if (system.args.length < 3 || system.args.length > 5) { if (system.args.length < 3 || system.args.length > 5) {
console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]'); console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
@ -17,24 +21,45 @@
} else { } else {
address = system.args[1]; address = system.args[1];
output = system.args[2]; output = system.args[2];
page.viewportSize = { width: 600, height: 600 }; page.viewportSize = {
width: 600,
height: 600
};
if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") { if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
size = system.args[3].split('*'); size = system.args[3].split('*');
page.paperSize = size.length === 2 ? { width: size[0], height: size[1], margin: '0px' } page.paperSize = size.length === 2 ? {
: { format: system.args[3], orientation: 'portrait', margin: '1cm' }; width: size[0],
height: size[1],
margin: '0px'
} : {
format: system.args[3],
orientation: 'portrait',
margin: '1cm'
};
} else if (system.args.length > 3 && system.args[3].substr(-2) === "px") { } else if (system.args.length > 3 && system.args[3].substr(-2) === "px") {
size = system.args[3].split('*'); size = system.args[3].split('*');
if (size.length === 2) { if (size.length === 2) {
pageWidth = parseInt(size[0], 10); pageWidth = parseInt(size[0], 10);
pageHeight = parseInt(size[1], 10); pageHeight = parseInt(size[1], 10);
page.viewportSize = { width: pageWidth, height: pageHeight }; page.viewportSize = {
page.clipRect = { top: 0, left: 0, width: pageWidth, height: pageHeight }; width: pageWidth,
height: pageHeight
};
page.clipRect = {
top: 0,
left: 0,
width: pageWidth,
height: pageHeight
};
} else { } else {
console.log("size:", system.args[3]); console.log("size:", system.args[3]);
pageWidth = parseInt(system.args[3], 10); pageWidth = parseInt(system.args[3], 10);
pageHeight = parseInt(pageWidth * 3 / 4, 10); // it's as good an assumption as any pageHeight = parseInt(pageWidth * 3 / 4, 10); // it's as good an assumption as any
console.log("pageHeight:", pageHeight); console.log("pageHeight:", pageHeight);
page.viewportSize = { width: pageWidth, height: pageHeight }; page.viewportSize = {
width: pageWidth,
height: pageHeight
};
} }
} }
if (system.args.length > 4) { if (system.args.length > 4) {
@ -45,7 +70,7 @@
console.log('Unable to load the address!'); console.log('Unable to load the address!');
phantom.exit(1); phantom.exit(1);
} else { } else {
window.setTimeout(function () { return window.setTimeout(function() {
page.render(output); page.render(output);
phantom.exit(); phantom.exit();
}, 200); }, 200);
@ -53,4 +78,7 @@
}); });
} }
}()); }).call(this);
/* eslint-enable */
//# sourceMappingURL=rasterize.js.map

View File

@ -0,0 +1,20 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the ResumeDetector class.
@module utils/resume-detector
@license MIT. See LICENSE.md for details.
*/
module.exports = function( rez ) {
if (rez.meta && rez.meta.format) { //&& rez.meta.format.substr(0, 5).toUpperCase() == 'FRESH'
return 'fresh';
} else if (rez.basics) {
return 'jrs';
} else {
return 'unk';
}
};

View File

@ -0,0 +1,62 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const _ = require('underscore');
module.exports = {
/**
Removes ignored or private fields from a resume object
@returns an object with the following structure:
{
scrubbed: the processed resume object
ignoreList: an array of ignored nodes that were removed
privateList: an array of private nodes that were removed
}
*/
scrubResume(rep, opts) {
const traverse = require('traverse');
const ignoreList = [];
const privateList = [];
const includePrivates = opts && opts.private;
const scrubbed = traverse( rep ).map(function() { // [^1]
if (!this.isLeaf) {
if ((this.node.ignore === true) || (this.node.ignore === 'true')) {
ignoreList.push(this.node);
this.delete();
} else if (((this.node.private === true) || (this.node.private === 'true')) && !includePrivates) {
privateList.push(this.node);
this.delete();
}
}
if (_.isArray(this.node)) { // [^2]
this.after(function() {
this.update(_.compact(this.node));
});
}
});
return {
scrubbed,
ingoreList: ignoreList,
privateList
};
}
};
// [^1]: As of v0.6.6, the NPM traverse library has a quirk when attempting
// to remove array elements directly using traverse's `this.remove`. See:
//
// https://github.com/substack/js-traverse/issues/48
//
// [^2]: The workaround is to use traverse's 'this.delete' to nullify the value
// first, followed by removal with something like _.compact.
//
// https://github.com/substack/js-traverse/issues/48#issuecomment-142607200
//

View File

@ -1,46 +1,34 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the SafeJsonLoader class. Definition of the SafeJsonLoader class.
@module syntax-error-ex.js @module utils/safe-json-loader
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
*/ */
const FS = require('fs');
const SyntaxErrorEx = require('./syntax-error-ex');
module.exports = function( file ) {
(function() { const ret = { };
var FS = require('fs')
, SyntaxErrorEx = require('./syntax-error-ex');
module.exports = function loadSafeJson( file ) {
var ret = { };
try { try {
ret.raw = FS.readFileSync( file, 'utf8' ); ret.raw = FS.readFileSync( file, 'utf8' );
ret.json = JSON.parse( ret.raw ); ret.json = JSON.parse( ret.raw );
} catch (err) {
}
catch( ex ) {
// If we get here, either FS.readFileSync or JSON.parse failed. // If we get here, either FS.readFileSync or JSON.parse failed.
// We'll return HMSTATUS.readError or HMSTATUS.parseError. // We'll return HMSTATUS.readError or HMSTATUS.parseError.
var retRaw = ret.raw && ret.raw.trim(); const retRaw = ret.raw && ret.raw.trim();
ret.ex = { ret.ex = {
operation: retRaw ? 'parse' : 'read', op: retRaw ? 'parse' : 'read',
inner: SyntaxErrorEx.is( ex ) ? (new SyntaxErrorEx( ex, retRaw )) : ex, inner:
file: file SyntaxErrorEx.is( err )
? (new SyntaxErrorEx( err, retRaw ))
: err,
file
}; };
} }
return ret; return ret;
}; };
}());

View File

@ -1,45 +1,44 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Safe spawn utility for HackMyResume / FluentCV. Safe spawn utility for HackMyResume / FluentCV.
@module safe-spawn.js @module utils/safe-spawn
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
*/ */
/** Safely spawn a process synchronously or asynchronously without throwing an
exception */
(function() { module.exports = function( cmd, args, isSync, callback, param ) {
module.exports = function( cmd, args, isSync ) {
try { try {
// .spawnSync not available on earlier Node.js, so default to .spawn
const spawn = require('child_process')[ isSync ? 'spawnSync' : 'spawn'];
const info = spawn(cmd, args);
var spawn = require('child_process')[ isSync? 'spawnSync' : 'spawn']; // Check for error depending on whether we're sync or async TODO: Promises
var info = spawn( cmd, args );
if (!isSync) { if (!isSync) {
info.on('error', function(err) { info.on('error', function(err) {
throw { if (typeof callback === 'function') {
cmd: 'wkhtmltopdf', callback(err, param);
inner: err }
};
}); });
} return;
else { } else {
if (info.error) { if (info.error) {
throw { if (typeof callback === 'function') {
cmd: 'wkhtmltopdf', callback(info.error, param);
inner: info.error }
}; return {cmd, inner: info.error};
} }
} }
} catch (ex) {
if (typeof callback === 'function') {
callback(ex, param);
} }
catch( ex ) { return ex;
} }
}; };
}());

View File

@ -1,65 +1,56 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Object string transformation. Object string transformation.
@module string-transformer.js @module utils/string-transformer
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
*/ */
const _ = require('underscore');
const moment = require('moment');
(function() {
var _ = require('underscore');
var moment = require('moment');
/** /**
Create a copy of this object in which all string fields have been run through Create a copy of this object in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder). a transformation function (such as a Markdown filter or XML encoder).
*/ */
module.exports = function( ret, filt, transformer ) { module.exports = function( ret, filt, transformer ) {
var that = this;
// TODO: refactor recursion // TODO: refactor recursion
function transformStringsInObject( obj, filters ) { var transformStringsInObject = function( obj, filters ) {
if (!obj) { return; }
if( !obj ) return; if (moment.isMoment(obj)) { return; }
if( moment.isMoment( obj ) ) return;
if (_.isArray( obj )) { if (_.isArray( obj )) {
obj.forEach( function(elem, idx, ar) { return obj.forEach(function(elem, idx, ar) {
if( typeof elem === 'string' || elem instanceof String ) if ((typeof elem === 'string') || elem instanceof String) {
ar[idx] = transformer( null, elem ); return ar[idx] = transformer( null, elem );
else if (_.isObject(elem)) } else if (_.isObject(elem)) {
transformStringsInObject( elem, filters ); return transformStringsInObject( elem, filters );
});
} }
else if (_.isObject( obj )) { });
Object.keys( obj ).forEach(function(k) { } else if (_.isObject( obj )) {
if( filters.length && _.contains(filters, k) ) return Object.keys( obj ).forEach(function(k) {
if (filters.length && _.contains(filters, k)) {
return; return;
var sub = obj[k];
if( typeof sub === 'string' || sub instanceof String ) {
obj[k] = transformer( k, sub );
} }
else if (_.isObject( sub )) const sub = obj[k];
transformStringsInObject( sub, filters ); if ((typeof sub === 'string') || sub instanceof String) {
return obj[k] = transformer( k, sub );
} else if (_.isObject( sub )) {
return transformStringsInObject( sub, filters );
}
}); });
} }
}
Object.keys( ret ).forEach(function(member){
if( !filt || !filt.length || !_.contains(filt, member) )
transformStringsInObject( ret[ member ], filt || [] );
});
return ret;
}; };
Object.keys( ret ).forEach(function(member) {
if (!filt || !filt.length || !_.contains(filt, member)) {
}()); return transformStringsInObject( ret[ member ], filt || [] );
}
});
return ret;
};

View File

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

@ -1,10 +1,14 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the SyntaxErrorEx class. Definition of the SyntaxErrorEx class.
@module syntax-error-ex.js @module utils/syntax-error-ex
@license MIT. See LICENSE.md for details. @license MIT. See LICENSE.md for details.
*/ */
(function() {
/** /**
@ -16,20 +20,24 @@ Definition of the SyntaxErrorEx class.
@class SyntaxErrorEx @class SyntaxErrorEx
*/ */
function SyntaxErrorEx( ex, rawData ) { class SyntaxErrorEx {
constructor( ex, rawData ) {
var lineNum = null, colNum = null; let JSONLint = require('json-lint');
var JSONLint = require('json-lint'); const lint = JSONLint(rawData, { comments: false });
var lint = JSONLint( rawData, { comments: false } ); if (lint.error) { [this.line, this.col] = Array.from([lint.line, lint.character]); }
this.line = (lint.error ? lint.line : '???'); if (!lint.error) {
this.col = (lint.error ? lint.character : '???'); JSONLint = require('jsonlint');
try {
JSONLint.parse(rawData);
} catch (err) {
this.line = (/on line (\d+)/.exec(err))[1];
}
}
}
} }
SyntaxErrorEx.is = function( ex ) {
return ex instanceof SyntaxError; // Return true if the supplied parameter is a JavaScript SyntaxError
}; SyntaxErrorEx.is = ex => ex instanceof SyntaxError;
module.exports = SyntaxErrorEx; module.exports = SyntaxErrorEx;
}());

View File

@ -1,3 +1,8 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Implementation of the 'analyze' verb for HackMyResume. Implementation of the 'analyze' verb for HackMyResume.
@module verbs/analyze @module verbs/analyze
@ -6,89 +11,79 @@ Implementation of the 'analyze' verb for HackMyResume.
(function(){ const HMEVENT = require('../core/event-codes');
const HMSTATUS = require('../core/status-codes');
const _ = require('underscore');
const ResumeFactory = require('../core/resume-factory');
const Verb = require('../verbs/verb');
var MKDIRP = require('mkdirp') /** An invokable resume analysis command. */
, PATH = require('path') class AnalyzeVerb extends Verb {
, HMEVENT = require('../core/event-codes') constructor() { super('analyze', _analyze); }
, HMSTATUS = require('../core/status-codes')
, _ = require('underscore')
, ResumeFactory = require('../core/resume-factory')
, Verb = require('../verbs/verb')
, chalk = require('chalk');
var AnalyzeVerb = module.exports = Verb.extend({
init: function() {
this._super('analyze');
},
invoke: function() {
this.stat( HMEVENT.begin, { cmd: 'analyze' });
analyze.apply( this, arguments );
this.stat( HMEVENT.end );
} }
}); module.exports = AnalyzeVerb;
/** /** Private workhorse for the 'analyze' command. */
Run the 'analyze' command. var _analyze = function( sources, dst, opts ) {
*/
function analyze( sources, dst, opts ) {
if( !sources || !sources.length )
throw { fluenterror: HMSTATUS.resumeNotFound, quit: true };
var nlzrs = _loadInspectors();
_.each(sources, function(src) {
var result = ResumeFactory.loadOne( src, {
format: 'FRESH', objectify: true
}, this);
if( result.fluenterror )
this.setError( result.fluenterror, result );
else
_analyze.call(this, result, nlzrs, opts );
}, this);
if (!sources || !sources.length) {
this.err(HMSTATUS.resumeNotFound, { quit: true });
return null;
} }
const nlzrs = _loadInspectors();
const results = _.map(sources, function(src) {
const r = ResumeFactory.loadOne(src, { format: 'FRESH', objectify: true, inner: {
private: opts.private === true
}
}, this);
if (opts.assert && this.hasError()) { return { }; }
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
return r;
} else {
return _analyzeOne.call(this, r, nlzrs, opts);
}
}
, this);
/** if (this.hasError() && !opts.assert) {
Analyze a single resume. this.reject(this.errorCode);
*/ } else if (!this.hasError()) {
function _analyze( resumeObject, nlzrs, opts ) { this.resolve(results);
var rez = resumeObject.rez; }
var safeFormat = return results;
(rez.meta && rez.meta.format && rez.meta.format.startsWith('FRESH')) ? };
'FRESH' : 'JRS';
/** Analyze a single resume. */
var _analyzeOne = function( resumeObject, nlzrs ) {
const { rez } = resumeObject;
const safeFormat =
rez.meta && rez.meta.format && rez.meta.format.startsWith('FRESH')
? 'FRESH' : 'JRS';
this.stat( HMEVENT.beforeAnalyze, { fmt: safeFormat, file: resumeObject.file }); this.stat( HMEVENT.beforeAnalyze, { fmt: safeFormat, file: resumeObject.file });
var info = _.mapObject( nlzrs, function(val, key) { const info = _.mapObject(nlzrs, (val) => val.run(rez));
return val.run( resumeObject.rez ); this.stat(HMEVENT.afterAnalyze, { info });
}); return info;
this.stat( HMEVENT.afterAnalyze, { info: info } ); };
}
/** var _loadInspectors = () =>
Load inspectors. ({
*/
function _loadInspectors() {
return {
totals: require('../inspectors/totals-inspector'), totals: require('../inspectors/totals-inspector'),
coverage: require('../inspectors/gap-inspector'), coverage: require('../inspectors/gap-inspector'),
keywords: require('../inspectors/keyword-inspector') keywords: require('../inspectors/keyword-inspector')
}; })
} ;
}());

View File

@ -1,3 +1,9 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Implementation of the 'build' verb for HackMyResume. Implementation of the 'build' verb for HackMyResume.
@module verbs/build @module verbs/build
@ -6,50 +12,46 @@ Implementation of the 'build' verb for HackMyResume.
(function() { const _ = require('underscore');
const PATH = require('path');
const MKDIRP = require('mkdirp');
const extend = require('extend');
var _ = require('underscore') const parsePath = require('parse-filepath');
, PATH = require('path') const RConverter = require('fresh-jrs-converter');
, FS = require('fs') const HMSTATUS = require('../core/status-codes');
, MD = require('marked') const HMEVENT = require('../core/event-codes');
, MKDIRP = require('mkdirp') const RTYPES = {
, extend = require('extend') FRESH: require('../core/fresh-resume'),
, parsePath = require('parse-filepath') JRS: require('../core/jrs-resume')
, RConverter = require('fresh-jrs-converter') };
, HMSTATUS = require('../core/status-codes') const _opts = require('../core/default-options');
, HMEVENT = require('../core/event-codes') const FRESHTheme = require('../core/fresh-theme');
, RTYPES = { FRESH: require('../core/fresh-resume'), const JRSTheme = require('../core/jrs-theme');
JRS: require('../core/jrs-resume') } const ResumeFactory = require('../core/resume-factory');
, _opts = require('../core/default-options') const _fmts = require('../core/default-formats');
, FRESHTheme = require('../core/fresh-theme') const Verb = require('../verbs/verb');
, JRSTheme = require('../core/jrs-theme')
, ResumeFactory = require('../core/resume-factory')
, _fmts = require('../core/default-formats')
, Verb = require('../verbs/verb');
var _err, _log, _rezObj;
//const _err = null;
//const _log = null;
let _rezObj = null;
//const build = null;
//const prep = null;
//const single = null;
//const verifyOutputs = null;
//const addFreebieFormats = null;
//const expand = null;
//const verifyTheme = null;
//const loadTheme = null;
/** An invokable resume generation command. */ /** An invokable resume generation command. */
var BuildVerb = module.exports = Verb.extend({ class BuildVerb extends Verb {
/** Create a new build verb. */ /** Create a new build verb. */
init: function() { constructor() { super('build', _build); }
this._super('build');
},
/** Invoke the Build command. */
invoke: function() {
this.stat( HMEVENT.begin, { cmd: 'build' } );
var ret = build.apply( this, arguments );
this.stat( HMEVENT.end );
return ret;
} }
});
module.exports = BuildVerb;
@ -58,110 +60,141 @@ Implementation of the 'build' verb for HackMyResume.
theme file, generate 0..N resumes in the desired formats. theme file, generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json". @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 dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern". @param opts Generation options.
@param logger Optional logging override.
*/ */
function build( src, dst, opts ) { var _build = function( src, dst, opts ) {
let err;
if (!src || !src.length) { if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, {quit: true}); this.err(HMSTATUS.resumeNotFound, {quit: true});
}
prep( src, dst, opts );
// Load input resumes as JSON...
var sheetObjects = ResumeFactory.load(src, {
format: null, objectify: false, quit: true, inner: { sort: _opts.sort }
}, this);
// Explicit check for any resume loading errors...
if( !sheetObjects ||
_.some( sheetObjects, function(so) { return so.fluenterror; } ) ) {
return null; return null;
} }
var sheets = sheetObjects.map(function(r) { return r.json; }); _prep.call(this, src, dst, opts);
// Load input resumes as JSON...
const sheetObjects = ResumeFactory.load(src, {
format: null, objectify: false, quit: true, inner: {
sort: _opts.sort,
private: _opts.private
}
}
, this);
// Explicit check for any resume loading errors...
const problemSheets = _.filter(sheetObjects, so => so.fluenterror);
if (problemSheets && problemSheets.length) {
problemSheets[0].quit = true; // can't go on
this.err(problemSheets[0].fluenterror, problemSheets[0]);
return null;
}
// Get the collection of raw JSON sheets
const sheets = sheetObjects.map(r => r.json);
// Load the theme... // Load the theme...
var theme; let theme = null;
this.stat(HMEVENT.beforeTheme, { theme: _opts.theme }); this.stat(HMEVENT.beforeTheme, { theme: _opts.theme });
try { try {
var tFolder = verifyTheme.call( this, _opts.theme ); const tFolder = _verifyTheme.call(this, _opts.theme);
theme = _opts.themeObj = loadTheme( tFolder ); if (tFolder.fluenterror) {
tFolder.quit = true;
this.err(tFolder.fluenterror, tFolder);
return;
} }
catch( ex ) { theme = (_opts.themeObj = _loadTheme(tFolder));
var newEx = { _addFreebieFormats(theme);
} catch (error) {
err = error;
const newEx = {
fluenterror: HMSTATUS.themeLoad, fluenterror: HMSTATUS.themeLoad,
inner: ex, inner: err,
attempted: _opts.theme attempted: _opts.theme,
quit: true
}; };
this.err(HMSTATUS.themeLoad, newEx); this.err(HMSTATUS.themeLoad, newEx);
return null; return null;
} }
this.stat( HMEVENT.afterTheme, { theme: theme });
this.stat(HMEVENT.afterTheme, {theme});
// Check for invalid outputs... // Check for invalid outputs...
var inv = verifyOutputs.call( this, dst, theme ); const inv = _verifyOutputs.call(this, dst, theme);
if (inv && inv.length) { if (inv && inv.length) {
this.err( HMSTATUS.invalidFormat, { data: inv, theme: theme } ); this.err(HMSTATUS.invalidFormat, {data: inv, theme, quit: true});
return null;
} }
// Merge input resumes, yielding a single source resume. //# Merge input resumes, yielding a single source resume...
var rez; let rez = null;
if (sheets.length > 1) { if (sheets.length > 1) {
var isFRESH = !sheets[0].basics; const isFRESH = !sheets[0].basics;
var mixed = _.any( sheets, function(s) { return isFRESH ? s.basics : !s.basics; }); const mixed = _.any(sheets, function(s) { if (isFRESH) { return s.basics; } else { return !s.basics; } });
this.stat( HMEVENT.beforeMerge, { f: _.clone(sheetObjects), mixed: mixed }); this.stat(HMEVENT.beforeMerge, { f: _.clone(sheetObjects), mixed });
if (mixed) { if (mixed) {
this.err(HMSTATUS.mixedMerge); this.err(HMSTATUS.mixedMerge);
} }
rez = _.reduceRight( sheets, function( a, b, idx ) { rez = _.reduceRight(sheets, ( a, b ) => extend( true, b, a ));
return extend( true, b, a );
});
this.stat(HMEVENT.afterMerge, { r: rez }); this.stat(HMEVENT.afterMerge, { r: rez });
} } else {
else {
rez = sheets[0]; rez = sheets[0];
} }
// Convert the merged source resume to the theme's format, if necessary // Convert the merged source resume to the theme's format, if necessary..
var orgFormat = rez.basics ? 'JRS' : 'FRESH'; const orgFormat = rez.basics ? 'JRS' : 'FRESH';
var toFormat = theme.render ? 'JRS' : 'FRESH'; const toFormat = theme.render ? 'JRS' : 'FRESH';
if (toFormat !== orgFormat) { if (toFormat !== orgFormat) {
this.stat(HMEVENT.beforeInlineConvert); this.stat(HMEVENT.beforeInlineConvert);
rez = RConverter[ 'to' + toFormat ]( rez ); rez = RConverter[ `to${toFormat}` ]( rez );
this.stat(HMEVENT.afterInlineConvert, { file: sheetObjects[0].file, fmt: toFormat }); this.stat(HMEVENT.afterInlineConvert, { file: sheetObjects[0].file, fmt: toFormat });
} }
// Add freebie formats to the theme // Announce the theme
addFreebieFormats( theme ); this.stat(HMEVENT.applyTheme, {r: rez, theme});
this.stat( HMEVENT.applyTheme, { r: rez, theme: theme });
// Load the resume into a FRESHResume or JRSResume object // Load the resume into a FRESHResume or JRSResume object
_rezObj = new (RTYPES[ toFormat ])().parseJSON( rez ); _rezObj = new (RTYPES[ toFormat ])().parseJSON( rez, {private: _opts.private} );
// Expand output resumes... // Expand output resumes...
var targets = expand( dst, theme ); const targets = _expand(dst, theme);
// Run the transformation! // Run the transformation!
_.each(targets, function(t) { _.each(targets, function(t) {
t.final = single.call( this, t, theme, targets ); if (this.hasError() && opts.assert) { return { }; }
}, this); t.final = _single.call(this, t, theme, targets);
if (t.final != null ? t.final.fluenterror : undefined) {
// Don't send the client back empty-handed t.final.quit = opts.assert;
return { sheet: _rezObj, targets: targets, processed: targets }; this.err(t.final.fluenterror, t.final);
} }
}
, this);
const results = {
sheet: _rezObj,
targets,
processed: targets
};
if (this.hasError() && !opts.assert) {
this.reject(results);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** /**
Prepare for a BUILD run. Prepare for a BUILD run.
*/ */
function prep( src, dst, opts ) { var _prep = function( src, dst, opts ) {
// Cherry-pick options //_opts = extend( true, _opts, opts ); // Cherry-pick options //_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern'; _opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
_opts.prettify = opts.prettify === true; _opts.prettify = opts.prettify === true;
_opts.private = opts.private === true;
_opts.noescape = opts.noescape === true;
_opts.css = opts.css; _opts.css = opts.css;
_opts.pdf = opts.pdf; _opts.pdf = opts.pdf;
_opts.wrap = opts.wrap || 60; _opts.wrap = opts.wrap || 60;
@ -171,12 +204,25 @@ Implementation of the 'build' verb for HackMyResume.
_opts.noTips = opts.noTips; _opts.noTips = opts.noTips;
_opts.debug = opts.debug; _opts.debug = opts.debug;
_opts.sort = opts.sort; _opts.sort = opts.sort;
_opts.wkhtmltopdf = opts.wkhtmltopdf;
const that = this;
// Set up callbacks for internal generators
_opts.onTransform = function(info) {
that.stat(HMEVENT.afterTransform, info);
};
_opts.beforeWrite = function(info) {
that.stat(HMEVENT.beforeWrite, info);
};
_opts.afterWrite = function(info) {
that.stat(HMEVENT.afterWrite, info);
};
// If two or more files are passed to the GENERATE command and the TO // If two or more files are passed to the GENERATE command and the TO
// keyword is omitted, the last file specifies the output file. // keyword is omitted, the last file specifies the output file.
( src.length > 1 && ( !dst || !dst.length ) ) && dst.push( src.pop() ); ( (src.length > 1) && ( !dst || !dst.length ) ) && dst.push( src.pop() );
};
}
@ -186,46 +232,44 @@ Implementation of the 'build' verb for HackMyResume.
@param targInfo Information for the target resume. @param targInfo Information for the target resume.
@param theme A FRESHTheme or JRSTheme object. @param theme A FRESHTheme or JRSTheme object.
*/ */
function single( targInfo, theme, finished ) { var _single = function( targInfo, theme, finished ) {
var ret, ex, f = targInfo.file; let ret = null;
let ex = null;
const f = targInfo.file;
try { try {
if( !targInfo.fmt ) { return; } if (!targInfo.fmt) {
var fType = targInfo.fmt.outFormat return { };
, fName = PATH.basename(f, '.' + fType) }
, theFormat; let theFormat = null;
this.stat(HMEVENT.beforeGenerate, { this.stat(HMEVENT.beforeGenerate, {
fmt: targInfo.fmt.outFormat, fmt: targInfo.fmt.outFormat,
file: PATH.relative(process.cwd(), f) file: PATH.relative(process.cwd(), f)
}); }
);
_opts.targets = finished;
// If targInfo.fmt.files exists, this format is backed by a document. // If targInfo.fmt.files exists, this format is backed by a document.
// Fluent/FRESH themes are handled here. // Fluent/FRESH themes are handled here.
if (targInfo.fmt.files && targInfo.fmt.files.length) { if (targInfo.fmt.files && targInfo.fmt.files.length) {
theFormat = _fmts.filter( theFormat = _fmts.filter( fmt => fmt.name === targInfo.fmt.outFormat)[0];
function(fmt) { return fmt.name === targInfo.fmt.outFormat; })[0]; MKDIRP.sync(PATH.dirname( f ));
MKDIRP.sync( PATH.dirname( f ) ); // Ensure dest folder exists;
_opts.targets = finished;
ret = theFormat.gen.generate(_rezObj, f, _opts); ret = theFormat.gen.generate(_rezObj, f, _opts);
}
// Otherwise this is an ad-hoc format (JSON, YML, or PNG) that every theme // Otherwise this is an ad-hoc format (JSON, YML, or PNG) that every theme
// gets "for free". // gets "for free".
else { } else {
theFormat = _fmts.filter( function(fmt) { theFormat = _fmts.filter( fmt => fmt.name === targInfo.fmt.outFormat)[0];
return fmt.name === targInfo.fmt.outFormat; const outFolder = PATH.dirname(f);
})[0];
var outFolder = PATH.dirname( f );
MKDIRP.sync(outFolder); // Ensure dest folder exists; MKDIRP.sync(outFolder); // Ensure dest folder exists;
ret = theFormat.gen.generate(_rezObj, f, _opts); ret = theFormat.gen.generate(_rezObj, f, _opts);
} }
}
catch( e ) { } catch (e) {
// Catch any errors caused by generating this file and don't let them
// propagate -- typically we want to continue processing other formats
// even if this format failed.
ex = e; ex = e;
} }
@ -233,40 +277,29 @@ Implementation of the 'build' verb for HackMyResume.
fmt: targInfo.fmt.outFormat, fmt: targInfo.fmt.outFormat,
file: PATH.relative(process.cwd(), f), file: PATH.relative(process.cwd(), f),
error: ex error: ex
});
if( ex ) {
if( ex.fluenterror )
this.err( ex.fluenterror, ex );
else
this.err( HMSTATUS.generateError, { inner: ex } );
}
return ret;
}
/**
Ensure that user-specified outputs/targets are valid.
*/
function verifyOutputs( targets, theme ) {
this.stat(HMEVENT.verifyOutputs, { targets: targets, theme: theme });
return _.reject(
targets.map( function( t ) {
var pathInfo = parsePath( t );
return {
format: pathInfo.extname.substr(1)
};
}),
function(t) {
return t.format === 'all' || theme.hasFormat( t.format );
} }
); );
if (ex) {
if (ex.fluenterror) {
ret = ex;
} else {
ret = {fluenterror: HMSTATUS.generateError, inner: ex};
} }
}
return ret;
};
/** Ensure that user-specified outputs/targets are valid. */
var _verifyOutputs = function( targets, theme ) {
this.stat(HMEVENT.verifyOutputs, {targets, theme});
return _.reject(targets.map( function( t ) {
const pathInfo = parsePath(t);
return {format: pathInfo.extname.substr(1)}; }),
t => (t.format === 'all') || theme.hasFormat( t.format ));
};
@ -279,7 +312,7 @@ Implementation of the 'build' verb for HackMyResume.
PNG template. PNG template.
@param theTheme A FRESHTheme or JRSTheme object. @param theTheme A FRESHTheme or JRSTheme object.
*/ */
function addFreebieFormats( theTheme ) { var _addFreebieFormats = function( theTheme ) {
// Add freebie formats (JSON, YAML, PNG) every theme gets... // Add freebie formats (JSON, YAML, PNG) every theme gets...
// Add HTML-driven PNG only if the theme has an HTML format. // Add HTML-driven PNG only if the theme has an HTML format.
theTheme.formats.json = theTheme.formats.json || { theTheme.formats.json = theTheme.formats.json || {
@ -296,7 +329,7 @@ Implementation of the 'build' verb for HackMyResume.
ext: 'yml', path: null, data: null ext: 'yml', path: null, data: null
}; };
} }
} };
@ -306,80 +339,103 @@ Implementation of the 'build' verb for HackMyResume.
@param dst An array of output files as specified by the user. @param dst An array of output files as specified by the user.
@param theTheme A FRESHTheme or JRSTheme object. @param theTheme A FRESHTheme or JRSTheme object.
*/ */
function expand( dst, theTheme ) { var _expand = function( dst, theTheme ) {
// Set up the destination collection. It's either the array of files passed // 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. // by the user or 'out/resume.all' if no targets were specified.
var destColl = (dst && dst.length && dst) || const destColl = (dst && dst.length && dst) || [PATH.normalize('out/resume.all')];
[PATH.normalize('out/resume.all')];
// Assemble an array of expanded target files... (can't use map() here) // Assemble an array of expanded target files... (can't use map() here)
var targets = []; const targets = [];
destColl.forEach(function(t) { destColl.forEach(function(t) {
const to = PATH.resolve(t);
var to = PATH.resolve(t), pa = parsePath(to),fmat = pa.extname || '.all'; const pa = parsePath(to);
const fmat = pa.extname || '.all';
return targets.push.apply( targets,
targets.push.apply( fmat === '.all'
targets, fmat === '.all' ? ? Object.keys( theTheme.formats ).map( function( k ) {
Object.keys( theTheme.formats ).map( function( k ) { const z = theTheme.formats[k];
var z = theTheme.formats[k];
return { file: to.replace( /all$/g, z.outFormat ), fmt: z }; return { file: to.replace( /all$/g, z.outFormat ), fmt: z };
}) : })
[{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]); : [{ 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; return targets;
} };
/** /**
Verify the specified theme name/path. Verify the specified theme name/path.
*/ */
function verifyTheme( themeNameOrPath ) { var _verifyTheme = function( themeNameOrPath ) {
var tFolder = PATH.join(
// First, see if this is one of the predefined FRESH themes. There are only a
// handful of these, but they may change over time, so we need to query
// the official source of truth: the fresh-themes repository, which mounts the
// themes conveniently by name to the module object, and which is embedded
// locally inside the HackMyResume installation.
let tFolder;
const themesObj = require('fresh-themes');
if (_.has(themesObj.themes, themeNameOrPath)) {
tFolder = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname, parsePath( require.resolve('fresh-themes') ).dirname,
'/themes/', '/themes/',
themeNameOrPath themeNameOrPath
); );
var exists = require('path-exists').sync; } else {
if( !exists( tFolder ) ) { // Otherwsie it's a path to an arbitrary FRESH or JRS theme sitting somewhere
// on the user's system (or, in the future, at a URI).
tFolder = PATH.resolve(themeNameOrPath); tFolder = PATH.resolve(themeNameOrPath);
if( !exists( tFolder ) ) {
this.err( HMSTATUS.themeNotFound, { data: _opts.theme } );
}
} }
// In either case, make sure the theme folder exists
const exists = require('path-exists').sync;
if (exists(tFolder)) {
return tFolder; return tFolder;
} else {
return {fluenterror: HMSTATUS.themeNotFound, data: _opts.theme};
} }
};
/** /**
Load the specified theme, which could be either a FRESH theme or a JSON Resume Load the specified theme, which could be either a FRESH theme or a JSON Resume
theme. theme (or both).
*/ */
function loadTheme( tFolder ) { var _loadTheme = function( tFolder ) {
const themeJsonPath = PATH.join(tFolder, 'theme.json'); // [^1]
const exists = require('path-exists').sync;
// Create a FRESH or JRS theme object // Create a FRESH or JRS theme object
var theTheme = _opts.theme.indexOf('jsonresume-theme-') > -1 ? const theTheme =
new JRSTheme().open(tFolder) : new FRESHTheme().open( tFolder ); exists(themeJsonPath)
? new FRESHTheme().open(tFolder)
: new JRSTheme().open(tFolder);
// Cache the theme object // Cache the theme object
_opts.themeObj = theTheme; _opts.themeObj = theTheme;
return theTheme; return theTheme;
} };
// FOOTNOTES
}()); // ------------------------------------------------------------------------------
// [^1] We don't know ahead of time whether this is a FRESH or JRS theme.
// However, all FRESH themes have a theme.json file, so we'll use that as a
// canary for now, as an interim solution.
//
// Unfortunately, with the exception of FRESH's theme.json, both FRESH and
// JRS themes are free-form and don't have a ton of reliable distinguishing
// marks, which makes a simple task like ad hoc theme detection harder than
// it should be to do cleanly.
//
// Another complicating factor is that it's possible for a theme to be BOTH.
// That is, a single set of theme files can serve as a FRESH theme -and- a
// JRS theme.
//
// TODO: The most robust way to deal with all these issues is with a strong
// theme validator. If a theme structure validates as a particular theme
// type, then for all intents and purposes, it IS a theme of that type.

View File

@ -1,3 +1,8 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Implementation of the 'convert' verb for HackMyResume. Implementation of the 'convert' verb for HackMyResume.
@module verbs/convert @module verbs/convert
@ -6,84 +11,161 @@ Implementation of the 'convert' verb for HackMyResume.
(function(){ const ResumeFactory = require('../core/resume-factory');
const Verb = require('../verbs/verb');
const HMSTATUS = require('../core/status-codes');
const _ = require('underscore');
const HMEVENT = require('../core/event-codes');
var ResumeFactory = require('../core/resume-factory') class ConvertVerb extends Verb {
, chalk = require('chalk') constructor() { super('convert', _convert); }
, Verb = require('../verbs/verb')
, HMSTATUS = require('../core/status-codes')
, _ = require('underscore')
, HMEVENT = require('../core/event-codes');
var ConvertVerb = module.exports = Verb.extend({
init: function() {
this._super('convert');
},
invoke: function() {
this.stat( HMEVENT.begin, { cmd: 'convert' });
convert.apply( this, arguments );
this.stat( HMEVENT.end );
} }
}); module.exports = ConvertVerb;
/** /** Private workhorse method. Convert 0..N resumes between FRESH and JRS
Convert between FRESH and JRS formats. formats. */
*/
function convert( srcs, dst, opts ) {
// Housekeeping var _convert = function( srcs, dst, opts ) {
if( !srcs || !srcs.length ) { throw { fluenterror: 6, quit: true }; }
// If no source resumes are specified, error out
let fmtUp;
if (!srcs || !srcs.length) {
this.err(HMSTATUS.resumeNotFound, { quit: true });
return null;
}
// If no destination resumes are specified, error out except for the special
// case of two resumes:
// hackmyresume CONVERT r1.json r2.json
if (!dst || !dst.length) { if (!dst || !dst.length) {
if (srcs.length === 1) { if (srcs.length === 1) {
throw { fluenterror: HMSTATUS.inputOutputParity, quit: true }; this.err(HMSTATUS.inputOutputParity, { quit: true });
} } else if (srcs.length === 2) {
else if( srcs.length === 2 ) {
dst = dst || []; dst.push( srcs.pop() ); dst = dst || []; dst.push( srcs.pop() );
} else {
this.err(HMSTATUS.inputOutputParity, { quit: true });
} }
else {
throw { fluenterror: HMSTATUS.inputOutputParity, quit: true };
}
}
if(srcs && dst && srcs.length && dst.length && srcs.length !== dst.length){
throw { fluenterror: HMSTATUS.inputOutputParity, quit: true };
} }
// Load source resumes // Different number of source and dest resumes? Error out.
_.each(srcs, function( src, idx ) { if (srcs && dst && srcs.length && dst.length && (srcs.length !== dst.length)) {
this.err(HMSTATUS.inputOutputParity, { quit: true });
}
// Validate the destination format (if specified)
//const targetVer = null;
if (opts.format) {
fmtUp = opts.format.trim().toUpperCase();
if (!_.contains(['FRESH','FRESCA','JRS','JRS@1','JRS@edge'], fmtUp)) {
this.err(HMSTATUS.invalidSchemaVersion, {data: opts.format.trim(), quit: true});
}
}
// freshVerRegex = require '../utils/fresh-version-regex'
// matches = fmtUp.match freshVerRegex()
// # null
// # [ 'JRS@1.0', 'JRS', '1.0', index: 0, input: 'FRESH' ]
// # [ 'FRESH', 'FRESH', undefined, index: 0, input: 'FRESH' ]
// if not matches
// @err HMSTATUS.invalidSchemaVersion, data: opts.format.trim(), quit: true
// targetSchema = matches[1]
// targetVer = matches[2] || '1'
// If any errors have occurred this early, we're done.
if (this.hasError()) {
this.reject(this.errorCode);
return null;
}
// Map each source resume to the converted destination resume
const results = _.map(srcs, function( src, idx ) {
// Convert each resume in turn
const r = _convertOne.call(this, src, dst, idx, fmtUp);
// Handle conversion errors
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
}
return r;
}
, this);
if (this.hasError() && !opts.assert) {
this.reject(results);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Private workhorse method. Convert a single resume. */
var _convertOne = function(src, dst, idx, targetSchema) {
// Load the resume // Load the resume
var rinfo = ResumeFactory.loadOne( src, { const rinfo = ResumeFactory.loadOne(src, {
format: null, objectify: true, throw: false format: null,
}); objectify: true,
inner: {
privatize: false
}
}
);
// If a load error occurs, report it and move on to the next file (if any) // If a load error occurs, report it and move on to the next file (if any)
if (rinfo.fluenterror) { if (rinfo.fluenterror) {
this.err( rinfo.fluenterror, rinfo ); this.stat(HMEVENT.beforeConvert, {
return; srcFile: src, //rinfo.file
srcFmt: '???',
dstFile: dst[idx],
dstFmt: '???',
error: true
}
);
//@err rinfo.fluenterror, rinfo
return rinfo;
} }
var s = rinfo.rez // Determine the resume's SOURCE format
, srcFmt = ((s.basics && s.basics.imp) || s.imp).orgFormat === 'JRS' ? // TODO: replace with detector component
'JRS' : 'FRESH' const { rez } = rinfo;
, targetFormat = srcFmt === 'JRS' ? 'FRESH' : 'JRS'; let srcFmt = '';
if (rez.meta && rez.meta.format) { //&& rez.meta.format.substr(0, 5).toUpperCase() == 'FRESH'
srcFmt = 'FRESH';
} else if (rez.basics) {
srcFmt = 'JRS';
} else {
rinfo.fluenterror = HMSTATUS.unknownSchema;
return rinfo;
}
this.stat(HMEVENT.beforeConvert, { srcFile: rinfo.file, srcFmt: srcFmt, dstFile: dst[idx], dstFmt: targetFormat }); // Determine the TARGET format for the conversion
const targetFormat = targetSchema || (srcFmt === 'JRS' ? 'FRESH' : 'JRS');
// Fire the beforeConvert event
this.stat(HMEVENT.beforeConvert, {
srcFile: rinfo.file,
srcFmt,
dstFile: dst[idx],
dstFmt: targetFormat
}
);
// Save it to the destination format // Save it to the destination format
s.saveAs( dst[idx], targetFormat ); try {
rez.saveAs(dst[idx], targetFormat);
}, this); } catch (err) {
if (err.badVer) {
return {fluenterror: HMSTATUS.invalidSchemaVersion, quit: true, data: err.badVer};
} }
}
return rez;
};
}());

View File

@ -1,3 +1,8 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Implementation of the 'create' verb for HackMyResume. Implementation of the 'create' verb for HackMyResume.
@module verbs/create @module verbs/create
@ -6,55 +11,74 @@ Implementation of the 'create' verb for HackMyResume.
(function(){ const MKDIRP = require('mkdirp');
const PATH = require('path');
const Verb = require('../verbs/verb');
const _ = require('underscore');
const HMSTATUS = require('../core/status-codes');
const HMEVENT = require('../core/event-codes');
var MKDIRP = require('mkdirp') class CreateVerb extends Verb {
, PATH = require('path')
, chalk = require('chalk')
, Verb = require('../verbs/verb')
, _ = require('underscore')
, HMSTATUS = require('../core/status-codes')
, HMEVENT = require('../core/event-codes');
constructor() { super('new', _create); }
var CreateVerb = module.exports = Verb.extend({
init: function() {
this._super('new');
},
invoke: function() {
this.stat( HMEVENT.begin, { cmd: 'create' });
create.apply( this, arguments );
this.stat( HMEVENT.begin, { cmd: 'convert' });
} }
});
module.exports = CreateVerb;
/** /** Create a new empty resume in either FRESH or JRS format. */
Create a new empty resume in either FRESH or JRS format. var _create = function( src, dst, opts ) {
*/
function create( src, dst, opts ) {
if( !src || !src.length ) if (!src || !src.length) {
throw { fluenterror: HMSTATUS.createNameMissing, quit: true }; this.err(HMSTATUS.createNameMissing, { quit: true });
return null;
}
_.each( src, function( t ) { const results = _.map(src, function( t ) {
var safeFmt = opts.format.toUpperCase(); if (opts.assert && this.hasError()) { return { }; }
const r = _createOne.call(this, t, opts);
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
}
return r;
}
, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Create a single new resume */
var _createOne = function( t, opts ) {
let ret, safeFmt;
try {
ret = null;
safeFmt = opts.format.toUpperCase();
this.stat(HMEVENT.beforeCreate, { fmt: safeFmt, file: t }); this.stat(HMEVENT.beforeCreate, { fmt: safeFmt, file: t });
MKDIRP.sync(PATH.dirname( t )); // Ensure dest folder exists; MKDIRP.sync(PATH.dirname( t )); // Ensure dest folder exists;
var RezClass = require('../core/' + safeFmt.toLowerCase() + '-resume' ); const RezClass = require(`../core/${safeFmt.toLowerCase()}-resume`);
RezClass.default().save(t); const newRez = RezClass.default();
this.stat( HMEVENT.afterCreate, { fmt: safeFmt, file: t } ); newRez.save(t);
}, this); ret = newRez;
} catch (err) {
ret = {
fluenterror: HMSTATUS.createError,
inner: err
};
} }
finally {
this.stat(HMEVENT.afterCreate, { fmt: safeFmt, file: t, isError: ret.fluenterror });
}
}()); return ret;
};

View File

@ -1,3 +1,8 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Implementation of the 'peek' verb for HackMyResume. Implementation of the 'peek' verb for HackMyResume.
@module verbs/peek @module verbs/peek
@ -6,78 +11,93 @@ Implementation of the 'peek' verb for HackMyResume.
(function(){ const Verb = require('../verbs/verb');
const _ = require('underscore');
const __ = require('lodash');
const safeLoadJSON = require('../utils/safe-json-loader');
const HMSTATUS = require('../core/status-codes');
const HMEVENT = require('../core/event-codes');
var Verb = require('../verbs/verb') class PeekVerb extends Verb {
, _ = require('underscore')
, __ = require('lodash')
, safeLoadJSON = require('../utils/safe-json-loader')
, HMSTATUS = require('../core/status-codes')
, HMEVENT = require('../core/event-codes');
constructor() { super('peek', _peek); }
var PeekVerb = module.exports = Verb.extend({
init: function() {
this._super('peek');
},
invoke: function() {
this.stat( HMEVENT.begin, { cmd: 'peek' } );
peek.apply( this, arguments );
this.stat( HMEVENT.end );
} }
});
module.exports = PeekVerb;
/** Peek at a resume, resume section, or resume field. */
var _peek = function( src, dst, opts ) {
if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, { quit: true });
return null;
}
const objPath = (dst && dst[0]) || '';
const results = _.map(src, function( t ) {
if (opts.assert && this.hasError()) { return { }; }
const tgt = _peekOne.call(this, t, objPath);
if (tgt.error) {
this.setError(tgt.error.fluenterror, tgt.error);
}
//tgt.error.quit = opts.assert
//@err tgt.error.fluenterror, tgt.error
return tgt;
}
, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** /** Peek at a single resume, resume section, or resume field. */
Peek at a resume, resume section, or resume field. var _peekOne = function( t, objPath ) {
*/
function peek( src, dst, opts ) {
if(!src || !src.length) throw {fluenterror: HMSTATUS.resumeNotFound};
var objPath = (dst && dst[0]) || '';
_.each( src, function( t ) {
// Fire the 'beforePeek' event 2nd, so we have error/warning/success
this.stat(HMEVENT.beforePeek, { file: t, target: objPath }); this.stat(HMEVENT.beforePeek, { file: t, target: objPath });
// Load the input file JSON 1st // Load the input file JSON 1st
var obj = safeLoadJSON( t ); const obj = safeLoadJSON(t);
// Fetch the requested object path (or the entire file) // Fetch the requested object path (or the entire file)
var tgt; let tgt = null;
if( !obj.ex ) if (!obj.ex) {
tgt = objPath ? __.get(obj.json, objPath) : obj.json; tgt = objPath ? __.get(obj.json, objPath) : obj.json;
}
//# safeLoadJSON can only return a READ error or a PARSE error
let pkgError = null;
if (obj.ex) {
const errCode = obj.ex.op === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError;
if (errCode === HMSTATUS.readError) {
obj.ex.quiet = true;
}
pkgError = {fluenterror: errCode, inner: obj.ex};
}
// Fire the 'afterPeek' event with collected info // Fire the 'afterPeek' event with collected info
this.stat(HMEVENT.afterPeek, { this.stat(HMEVENT.afterPeek, {
file: t, file: t,
requested: objPath, requested: objPath,
target: tgt, target: obj.ex ? undefined : tgt,
error: obj.ex error: pkgError
});
// safeLoadJSON can only return a READ error or a PARSE error
if( obj.ex ) {
var errCode = obj.ex.operation === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError;
if( errCode === HMSTATUS.readError )
obj.ex.quiet = true;
this.setError( errCode, obj.ex );
this.err( errCode, obj.ex );
} }
);
}, this); return {
val: obj.ex ? undefined : tgt,
} error: pkgError
};
};
}());

View File

@ -1,3 +1,8 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Implementation of the 'validate' verb for HackMyResume. Implementation of the 'validate' verb for HackMyResume.
@module verbs/validate @module verbs/validate
@ -6,98 +11,103 @@ Implementation of the 'validate' verb for HackMyResume.
(function() { const Verb = require('../verbs/verb');
const HMSTATUS = require('../core/status-codes');
const HMEVENT = require('../core/event-codes');
const _ = require('underscore');
var FS = require('fs'); const safeLoadJSON = require('../utils/safe-json-loader');
var ResumeFactory = require('../core/resume-factory');
var SyntaxErrorEx = require('../utils/syntax-error-ex');
var chalk = require('chalk');
var Verb = require('../verbs/verb');
var HMSTATUS = require('../core/status-codes');
var HMEVENT = require('../core/event-codes');
var _ = require('underscore');
/** An invokable resume validation command. */ /** An invokable resume validation command. */
var ValidateVerb = module.exports = Verb.extend({ class ValidateVerb extends Verb {
constructor() { super('validate', _validate); }
init: function() {
this._super('validate');
},
invoke: function() {
this.stat( HMEVENT.begin, { cmd: 'validate' } );
validate.apply( this, arguments );
this.stat( HMEVENT.end );
} }
});
// Validate 1 to N resumes in FRESH or JSON Resume format.
var _validate = function(sources, unused, opts) {
/** Validate 1 to N resumes in FRESH or JSON Resume format. */ if (!sources || !sources.length) {
function validate( sources, unused, opts ) { this.err(HMSTATUS.resumeNotFoundAlt, {quit: true});
return null;
}
if( !sources || !sources.length ) const validator = require('is-my-json-valid');
throw { fluenterror: HMSTATUS.resumeNotFoundAlt, quit: true }; const schemas = {
fresh: require('fresh-resume-schema'),
var validator = require('is-my-json-valid');
var schemas = {
fresh: require('fresca'),
jars: require('../core/resume.json') jars: require('../core/resume.json')
}; };
var resumes = ResumeFactory.load( sources, { const results = _.map(sources, function(t) {
format: null, const r = _validateOne.call(this, t, validator, schemas, opts);
objectify: false if (r.error) { this.err(r.error.fluenterror, r.error); }
}, this ); return r;
// Validate input resumes. Return a { file: <f>, isValid: <v>} object for
// each resume (valid, invalid, or broken).
return resumes.map( function( src ) {
var ret = { file: src, isValid: false };
// If there was an error reading the resume
if( src.fluenterror ) {
if( opts.assert ) throw src;
this.setError( src.fluenterror, src );
return ret;
} }
, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
module.exports = ValidateVerb;
/**
Validate a single resume.
@returns {
file: <fileName>,
isValid: <validFlag>,
status: <validationStatus>,
violations: <validationErrors>,
schema: <schemaType>,
error: <errorObject>
}
*/
var _validateOne = function(t, validator, schemas) {
const ret = {file: t, isValid: false, status: 'unknown', schema: '-----'};
// Successfully read the resume. Now parse it as JSON.
var json = src.json, fmt = json.basics ? 'jrs' : 'fresh', errors = [];
try { try {
var validate = validator( schemas[ fmt ], { // Note [1]
formats: {
date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/
}
});
ret.isValid = validate( json ); // Read and parse the resume JSON. Won't throw.
if( !ret.isValid ) { const obj = safeLoadJSON(t);
errors = validate.errors;
// If success, validate the resume against the schema
if (!obj.ex) {
if (obj.json.basics) { ret.schema = 'jars'; } else { ret.schema = 'fresh'; }
const validate = validator(schemas[ ret.schema ], // Note [1]
{formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }});
ret.isValid = validate(obj.json);
ret.status = ret.isValid ? 'valid' : 'invalid';
if (!ret.isValid) { ret.violations = validate.errors; }
// If failure, package JSON read/parse errors
} else {
let errCode;
if (obj.ex.op === 'parse') {
errCode = HMSTATUS.parseError;
ret.status = 'broken';
} else {
errCode = HMSTATUS.readError;
ret.status = 'missing';
}
ret.error = {
fluenterror: errCode,
inner: obj.ex.inner,
quiet: errCode === HMSTATUS.readError
};
} }
} catch (err) {
// Package any unexpected exceptions
ret.error = {fluenterror: HMSTATUS.validateError, inner: err};
} }
catch( exc ) {
this.stat(HMEVENT.afterValidate, ret);
return ret; return ret;
} };
this.stat( HMEVENT.afterValidate, { file: src.file, isValid: ret.isValid,
fmt: fmt.replace( 'jars', 'JSON Resume' ), errors: errors });
if( opts.assert && !ret.isValid ) {
throw { fluenterror: HMSTATUS.invalid, shouldExit: true };
}
return ret;
}, this);
}
}());

View File

@ -1,3 +1,8 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/** /**
Definition of the Verb class. Definition of the Verb class.
@module verbs/verb @module verbs/verb
@ -6,91 +11,105 @@ Definition of the Verb class.
(function(){ const EVENTS = require('events');
const HMEVENT = require('../core/event-codes');
const Promise = require('pinkie-promise');
// Use J. Resig's nifty class implementation
var Class = require( '../utils/class' )
, EVENTS = require('events');
/** /**
An instantiation of a HackMyResume command. An abstract invokable verb.
Provides base class functionality for verbs. Provide common services such as
error handling, event management, and promise support.
@class Verb @class Verb
*/ */
var Verb = module.exports = Class.extend({
class Verb {
/** /** Constructor. Automatically called at creation. */
Constructor. Automatically called at creation. constructor( moniker, workhorse ) {
*/
init: function( moniker ) {
this.moniker = moniker; this.moniker = moniker;
this.workhorse = workhorse;
this.emitter = new EVENTS.EventEmitter(); this.emitter = new EVENTS.EventEmitter();
},
/**
Forward subscriptions to the event emitter.
*/
on: function() {
this.emitter.on.apply( this.emitter, arguments );
},
/**
Fire an arbitrary event, scoped to "hmr:".
*/
fire: function(evtName, payload) {
payload = payload || { };
payload.cmd = this.moniker;
this.emitter.emit( 'hmr:' + evtName, payload );
return true;
},
/**
Handle an error condition.
*/
err: function( errorCode, payload, hot ) {
payload = payload || { };
payload.sub = payload.fluenterror = errorCode;
payload.throw = hot;
this.fire( 'error', payload );
if( hot ) throw payload;
return true;
},
/**
Fire the 'hmr:status' error event.
*/
stat: function( subEvent, payload ) {
payload = payload || { };
payload.sub = subEvent;
this.fire('status', payload);
return true;
},
/**
Associate error info with the invocation.
*/
setError: function( code, obj ) {
this.errorCode = code;
this.errorObj = obj;
} }
}); /** Invoke the command. */
invoke() {
}()); // Sent the 'begin' notification for this verb
this.stat(HMEVENT.begin, {cmd: this.moniker});
// Prepare command arguments
const argsArray = Array.prototype.slice.call(arguments);
// Create a promise for this verb instance
const that = this;
return this.promise = new Promise(function(res, rej) {
that.resolve = res;
that.reject = rej;
that.workhorse.apply(that, argsArray);
});
}
/** Forward subscriptions to the event emitter. */
on() { return this.emitter.on.apply(this.emitter, arguments); }
/** Fire an arbitrary event, scoped to "hmr:". */
fire(evtName, payload) {
payload = payload || { };
payload.cmd = this.moniker;
this.emitter.emit(`hmr:${evtName}`, payload);
return true;
}
/** Handle an error condition. */
err( errorCode, payload, hot ) {
payload = payload || { };
payload.sub = (payload.fluenterror = errorCode);
payload.throw = hot;
this.setError(errorCode, payload);
if (payload.quit) {
this.reject(errorCode);
}
this.fire('error', payload);
if (hot) {
throw payload;
}
return true;
}
/** Fire the 'hmr:status' error event. */
stat( subEvent, payload ) {
payload = payload || { };
payload.sub = subEvent;
this.fire('status', payload);
return true;
}
/** Has an error occurred during this verb invocation? */
hasError() { return this.errorCode || this.errorObj; }
/** Associate error info with the invocation. */
setError( code, obj ) {
this.errorCode = code;
this.errorObj = obj;
}
}
module.exports = Verb;

12
test/all.js Normal file
View File

@ -0,0 +1,12 @@
var MKDIRP = require('mkdirp');
var PATH = require('path');
MKDIRP( PATH.join( __dirname, '/sandbox' ) );
require('./scripts/test-cli');
require('./scripts/test-fresh-sheet');
require('./scripts/test-jrs-sheet');
require('./scripts/test-themes');
require('./scripts/test-verbs');
require('./scripts/test-output');
require('./scripts/test-dates');

View File

@ -0,0 +1,204 @@
* {
box-sizing: border-box;
}
main {
display: block;
}
body {
font-family: 'Helvetica Neue', 'Helvetica', 'Segoe UI', 'Calibri', 'sans-serif';
font-size: 15px;
color: #333;
line-height: 1.42857143;
background-color: #F0F0F0;
margin: 0;
padding: 0;
}
/* Typical page borders are awkward when rendered to PDF. */
body.pdf {
background-color: #FFFFFF;
}
/* Adobe or wkhtmltopdf has issues with the <main> tag, so we use <div> for
the PDF case, <main> for the HTML case, and style both via an ID. */
#main {
background-color: #FFF;
margin: 10px;
padding: 10px;
border: 1px solid #E6E6E6;
}
body.pdf > #main {
border: none;
}
#container > header {
padding-top: 6em;
padding-bottom: 1em;
}
body.pdf #container > header {
padding: 0;
}
#main > #container > section {
margin-left: 5em;
position: relative;
display: block;
}
span.fa
{
position: absolute;
top: 4px;
left: -50px;
font-size: 30px;
color: #BFC1C3;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #EEE;
}
.tenure, .keywords {
font-size: 75%;
}
h1 {
margin: 0;
font-size: 46px;
}
h2 {
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
h3 {
margin-bottom: 0;
font-size: 18px;
}
a, a:visited {
color: #428BCA;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.defunct {
color: #989898;
font-weight: bold;
}
#summary > p > strong {
font-size: 1.25em;
}
.label-keyword {
display: inline-block;
background: #7eb0db;
color: white;
font-size: 0.9em;
padding: 5px;
border: 1px solid #357ebd;
border-radius: 5px;
margin-top: 2px;
}
.card-skills {
position: relative;
}
.card-nested {
min-height: 0;
margin-bottom: 10px;
border-width: 1px 0 0 0;
}
.card {
background: #FFF;
border-radius: 3px;
padding: 10px;
}
.skill-level {
border-radius: 3px;
position: absolute;
top: 10px;
bottom: 10px;
left: 0;
width: 10px;
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5);
}
.skill-level .skill-progress {
position: absolute;
border-radius: 3px;
bottom: 0;
width: 100%;
-webkit-transition: height 1s ease;
}
.skill-level .skill-progress.beginner {
height: 50%;
background: #e74c3c;
}
.skill-level .skill-progress.intermediate {
height: 70%;
background: #f1c40f;
}
.skill-level .skill-progress.advanced {
height: 80%;
background: #428bca;
}
.skill-level .skill-progress.master {
height: 95%;
background: #5cb85c;
}
.skill-info {
margin-left: 10px;
}
@media (max-width: 480px) {
.skill-info {
margin-left: 20px;
}
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.card-skills {
position: relative;
}
.space-top {
margin-top: 10px;
}
#container {
max-width: 800px;
margin: 0 auto;
}
#elevator-pitch {
text-align: center;
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
.res-label {
font-style: italic;
}

View File

@ -0,0 +1,203 @@
* {
box-sizing: border-box;
}
main {
display: block;
}
body {
font-family: 'Helvetica Neue', 'Helvetica', 'Segoe UI', 'Calibri', 'sans-serif';
font-size: 15px;
color: #333;
line-height: 1.42857143;
background-color: #F0F0F0;
margin: 0;
padding: 0;
}
/* Typical page borders are awkward when rendered to PDF. */
body {
background-color: #FFFFFF;
}
/* Adobe or wkhtmltopdf has issues with the <main> tag, so we use <div> for
the PDF case, <main> for the HTML case, and style both via an ID. */
#main {
background-color: #FFF;
margin: 10px;
padding: 10px;
border: 1px solid #E6E6E6;
}
body > #main {
border: none;
}
#container > header {
padding-top: 6em;
padding-bottom: 1em;
}
body.pdf #container > header {
padding: 0;
}
#main > #container > section {
margin-left: 5em;
position: relative;
display: block;
}
span.fa
{
position: absolute;
top: 4px;
left: -50px;
font-size: 30px;
color: #BFC1C3;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #EEE;
}
.tenure, .keywords {
font-size: 75%;
}
h1 {
margin: 0;
font-size: 46px;
}
h2 {
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
h3 {
margin-bottom: 0;
font-size: 18px;
}
a, a:visited {
color: #428BCA;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.defunct {
color: #989898;
font-weight: bold;
}
#summary > p > strong {
font-size: 1.25em;
}
.label-keyword {
display: inline-block;
background: #7eb0db;
color: white;
font-size: 0.9em;
padding: 5px;
border: 1px solid #357ebd;
border-radius: 5px;
margin-top: 2px;
}
.card-skills {
position: relative;
}
.card-nested {
min-height: 0;
margin-bottom: 10px;
border-width: 1px 0 0 0;
}
.card {
background: #FFF;
border-radius: 3px;
padding: 10px;
}
.skill-level {
border-radius: 3px;
position: absolute;
top: 10px;
bottom: 10px;
left: 0;
width: 10px;
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5);
}
.skill-level .skill-progress {
position: absolute;
border-radius: 3px;
bottom: 0;
width: 100%;
-webkit-transition: height 1s ease;
}
.skill-level .skill-progress.beginner {
height: 50%;
background: #e74c3c;
}
.skill-level .skill-progress.intermediate {
height: 70%;
background: #f1c40f;
}
.skill-level .skill-progress.advanced {
height: 80%;
background: #428bca;
}
.skill-level .skill-progress.master {
height: 95%;
background: #5cb85c;
}
.skill-info {
margin-left: 10px;
}
@media (max-width: 480px) {
.skill-info {
margin-left: 20px;
}
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.card-skills {
position: relative;
}
.space-top {
margin-top: 10px;
}
#container {
max-width: 800px;
margin: 0 auto;
}
#elevator-pitch {
text-align: center;
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
.res-label {
font-style: italic;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,709 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jane Q. Fullstacker</title>
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,600italic,700,700italic'
rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<style>
* {
box-sizing: border-box;
}
main {
display: block;
}
body {
font-family: 'Helvetica Neue', 'Helvetica', 'Segoe UI', 'Calibri', 'sans-serif';
font-size: 15px;
color: #333;
line-height: 1.42857143;
background-color: #F0F0F0;
margin: 0;
padding: 0;
}
/* Typical page borders are awkward when rendered to PDF. */
body.pdf {
background-color: #FFFFFF;
}
/* Adobe or wkhtmltopdf has issues with the <main> tag, so we use <div> for
the PDF case, <main> for the HTML case, and style both via an ID. */
#main {
background-color: #FFF;
margin: 10px;
padding: 10px;
border: 1px solid #E6E6E6;
}
body.pdf > #main {
border: none;
}
#container > header {
padding-top: 6em;
padding-bottom: 1em;
}
body.pdf #container > header {
padding: 0;
}
#main > #container > section {
margin-left: 5em;
position: relative;
display: block;
}
span.fa
{
position: absolute;
top: 4px;
left: -50px;
font-size: 30px;
color: #BFC1C3;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #EEE;
}
.tenure, .keywords {
font-size: 75%;
}
h1 {
margin: 0;
font-size: 46px;
}
h2 {
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
h3 {
margin-bottom: 0;
font-size: 18px;
}
a, a:visited {
color: #428BCA;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.defunct {
color: #989898;
font-weight: bold;
}
#summary > p > strong {
font-size: 1.25em;
}
.label-keyword {
display: inline-block;
background: #7eb0db;
color: white;
font-size: 0.9em;
padding: 5px;
border: 1px solid #357ebd;
border-radius: 5px;
margin-top: 2px;
}
.card-skills {
position: relative;
}
.card-nested {
min-height: 0;
margin-bottom: 10px;
border-width: 1px 0 0 0;
}
.card {
background: #FFF;
border-radius: 3px;
padding: 10px;
}
.skill-level {
border-radius: 3px;
position: absolute;
top: 10px;
bottom: 10px;
left: 0;
width: 10px;
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5);
}
.skill-level .skill-progress {
position: absolute;
border-radius: 3px;
bottom: 0;
width: 100%;
-webkit-transition: height 1s ease;
}
.skill-level .skill-progress.beginner {
height: 50%;
background: #e74c3c;
}
.skill-level .skill-progress.intermediate {
height: 70%;
background: #f1c40f;
}
.skill-level .skill-progress.advanced {
height: 80%;
background: #428bca;
}
.skill-level .skill-progress.master {
height: 95%;
background: #5cb85c;
}
.skill-info {
margin-left: 10px;
}
@media (max-width: 480px) {
.skill-info {
margin-left: 20px;
}
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.card-skills {
position: relative;
}
.space-top {
margin-top: 10px;
}
#container {
max-width: 800px;
margin: 0 auto;
}
#elevator-pitch {
text-align: center;
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
.res-label {
font-style: italic;
}
</style>
</head>
<body>
<main id="main">
<div id="container">
<header>
<h1>Jane Q. Fullstacker</h1>
<div id="contact"></div>
</header>
<hr>
<section id="summary">
<header> <span class="fa fa-lg fa-info"></span>
<h2>info</h2>
</header> <strong>Imaginary full-stack software developer with 6+ years industry experience</strong> specializing
in cloud-driven web applications and middleware. A native of southern CA,
Jane enjoys hiking, mystery novels, and the company of Rufus, her two year
old beagle.</section>
<hr>
<section id="skills">
<header>
<h2>Skills</h2>
</header> <span class="fa fa-lg fa-code"></span>
<ul class="list-unstyled">
<li class="card card-nested card-skills">
<div class="skill-level" rel="tooltip" title="advanced" data-placement="left">
<div class="skill-progress advanced"></div>
</div>
<div class="skill-info"> <strong>Web</strong>
<div class="space-top labels"> <span class="label label-keyword">JavaScript</span>
<span class="label label-keyword">HTML 5</span>
<span
class="label label-keyword">CSS</span> <span class="label label-keyword">LAMP</span>
<span class="label label-keyword">MVC</span>
<span
class="label label-keyword">REST</span>
</div>
</div>
</li>
<li class="card card-nested card-skills">
<div class="skill-level" rel="tooltip" title="master" data-placement="left">
<div class="skill-progress master"></div>
</div>
<div class="skill-info"> <strong>JavaScript</strong>
<div class="space-top labels"> <span class="label label-keyword">Node.js</span>
<span class="label label-keyword">Angular.js</span>
<span
class="label label-keyword">jQuery</span> <span class="label label-keyword">Bootstrap</span>
<span class="label label-keyword">React.js</span>
<span
class="label label-keyword">Backbone.js</span>
</div>
</div>
</li>
<li class="card card-nested card-skills">
<div class="skill-level" rel="tooltip" title="intermediate" data-placement="left">
<div class="skill-progress intermediate"></div>
</div>
<div class="skill-info"> <strong>Database</strong>
<div class="space-top labels"> <span class="label label-keyword">MySQL</span>
<span class="label label-keyword">PostgreSQL</span>
<span
class="label label-keyword">NoSQL</span> <span class="label label-keyword">ORM</span>
<span class="label label-keyword">Hibernate</span>
</div>
</div>
</li>
<li class="card card-nested card-skills">
<div class="skill-level" rel="tooltip" title="intermediate" data-placement="left">
<div class="skill-progress intermediate"></div>
</div>
<div class="skill-info"> <strong>Cloud</strong>
<div class="space-top labels"> <span class="label label-keyword">AWS</span>
<span class="label label-keyword">EC2</span>
<span
class="label label-keyword">RDS</span> <span class="label label-keyword">S3</span>
<span class="label label-keyword">Azure</span>
<span
class="label label-keyword">Dropbox</span>
</div>
</div>
</li>
<li class="card card-nested card-skills">
<div class="skill-level" rel="tooltip" title="beginner" data-placement="left">
<div class="skill-progress beginner"></div>
</div>
<div class="skill-info"> <strong>Project</strong>
<div class="space-top labels"> <span class="label label-keyword">Agile</span>
<span class="label label-keyword">TFS</span>
<span
class="label label-keyword">JIRA</span> <span class="label label-keyword">GitHub</span>
<span class="label label-keyword">Unified Process</span>
<span
class="label label-keyword">MS Project</span>
</div>
</div>
</li>
</ul>
</section>
<hr>
<section id="employment">
<header> <span class="fa fa-lg fa-building"></span>
<h2>employment</h2>
</header>
<div>
<h3><em>Head Code Ninja</em>,
<a href="https://area52.io/does-not-exist">Area 52</a>
</h3>
<span class="tenure">2013-09 — Present</span>
| <span class="keywords">Agile PM C C++ R OpenGL Boost MySQL PostgreSQL JIRA </span>
<p>
<p>Development team manager for <a href="https://en.wikipedia.org/wiki/Vaporware"><strong>Quantum Diorama</strong></a>,
a distributed molecular modeling and analysis suite for Linux and OS X.</p>
</p>
<ul>
<li>Managed a 5-person development team</li>
<li>Accomplishment 2</li>
<li>Etc.</li>
</ul>
</div>
<div>
<h3><em>Principal Developer</em>,
<a href="https://en.wikipedia.org/wiki/Better_Off_Ted#Plot">Veridian Dynamics</a>
</h3>
<span class="tenure">2011-07 — 2013-08</span>
| <span class="keywords">C++ C Linux R Clojure </span>
<p>
<p>Developer on numerous projects culminating in technical lead role for
the <a href="http://betteroffted.wikia.com/wiki/Jabberwocky">Jabberwocky project</a> and
promotion to principal developer.</p>
</p>
<ul>
<li>Managed a 5-person development team</li>
<li>Accomplishment 2</li>
<li>Etc.</li>
</ul>
</div>
<div>
<h3><em>IT Administrator</em>,
Stark Industries
</h3>
<span class="tenure">2008-10 — 2011-06</span>
| <span class="keywords">Novell Active Directory Linux Windows </span>
<p>
<p>Junior programmer with heavy code responsibilities. Promoted to intermediate
role after 6 months.</p>
</p>
<ul>
<li>Promoted to intermediate developer after 6 months</li>
<li>Accomplishment 2</li>
<li>Etc.</li>
</ul>
</div>
<div>
<h3><em>Intern</em>,
Dunder Mifflin
</h3>
<span class="tenure">2008-06 — 2008-09</span>
| <span class="keywords">Novell Active Directory Linux Windows </span>
<p>
<p>Performed IT administration and deployments for Dunder Mifflin.</p>
</p>
<ul>
<li>Supervised roll-out of Dunder Mifflin Infinity website.</li>
<li>Performed mission-critical system backups and</li>
<li>Etc.</li>
</ul>
</div>
</section>
<hr>
<section id="projects">
<header> <span class="fa fa-lg fa-star"></span>
<h2>projects</h2>
</header>
<div>
<h3><em>Contributor</em>,
<a href="https://fluentdesk.com/hackmyresume">HackMyResume</a>
</h3>
<span class="tenure">2015-09 — Present</span>
| <span class="keywords">JavaScript Node.js cross-platform JSON </span>
<p>Exemplar user for HackMyResume and FluentCV!</p>
</div>
<div>
<h3><em>Co-creator</em>,
<a href="http://project.website.com">Augmented Android</a>
</h3>
<span class="tenure">2012-02 — 2014-01</span>
| <span class="keywords">Android Java Xamarin </span>
<p>Performed flagship product conceptualization and development.</p>
</div>
<div>
<h3><em>Creator</em>,
<a href="http://myblog.jane.com/blog">Blog</a>
</h3>
<span class="keywords">Jekyll Ruby HTML 5 JavaScript HTTP LAMP </span>
<p>Conceptualization, design, development, and deployment.</p>
</div>
</section>
<hr>
<section id="education">
<header> <span class="fa fa-lg fa-mortar-board"></span>
<h2>education</h2>
</header>
<div>
<h3><em>BSCS</em>,
<a href="https://www.cornell.edu/">Cornell University</a>
</h3>
<span class="tenure">2005-09 — 2008-05</span>
| <span class="keywords">Course 1 Course 2 Course 2 </span>
<p>A multiline summary of the education.</p>
</div>
<div>
<h3>
<a href="https://en.wikipedia.org/wiki/Medfield_College">Medfield College</a>
</h3>
<span class="tenure">2003-09 — 2005-06</span>
| <span class="keywords">Course 1 Course 2 Course 2 </span>
<p>A multiline summary of the education.</p>
</div>
</section>
<hr>
<section id="governance">
<header> <span class="fa fa-lg fa-balance-scale"></span>
<h2>governance</h2>
</header>
<div>
<h3><em>Member</em>,
<a href="http://themommiesnetwork.org">The Mommies Network</a>
</h3>
<span class="tenure">2008-02 — 2010-01</span>
<p>Since 2008 I&#39;ve been a full-time member of the board of directors
for TMN.</p>
</div>
<div>
<h3><em>Academic Contributor</em>,
<a href="https://www.khronos.org">Khronos Group</a>
</h3>
<span class="tenure">2015-01 — Present</span>
<ul>
<li>Participated in GORFF standardization process (Draft 2).</li>
</ul>
</div>
</section>
<hr>
<section id="service">
<header> <span class="fa fa-lg fa-child"></span>
<h2>service</h2>
</header>
<div>
<h3><em>Technical Consultant</em>,
<a href="http://technology-for-tots.org">Technology for Tots</a>
</h3>
<span class="tenure">2003-11 — 2005-06</span>
<p>
<p>Summary of this volunteer stint.</p>
</p>
<ul>
<li>Accomplishment 1</li>
<li>Accomplishment 2</li>
<li>etc</li>
</ul>
</div>
<div>
<h3><em>NCO</em>,
<a href="http://www.usar.army.mil/">US Army Reserves</a>
</h3>
<span class="tenure">1999-11 — 2003-06</span>
<p>
<p>Summary of this military stint.</p>
</p>
<ul>
<li>Accomplishment 1</li>
<li>Accomplishment 2</li>
<li>etc</li>
</ul>
</div>
</section>
<hr>
<section id="affiliation">
<header> <span class="fa fa-lg fa-share-alt"></span>
<h2>affiliation</h2>
</header>
<div>
<h3><em>Member</em>,
<a href="https://www.ieee.org/index.html">IEEE</a>
</h3>
<span class="tenure">2013-06 — Present</span>
<p>Member in good standing since 2013-06.</p>
</div>
<div>
<h3><em>Member</em>,
<a href="https://developer.apple.com/">Apple Developer Network</a>
</h3>
<span class="tenure">??? — Present</span>
<p>Member of the <a href="https://developer.apple.com/">Apple Developer program</a> since
2008.</p>
</div>
<div>
<h3><em>Subscriber</em>,
<a href="https://msdn.microsoft.com">MSDN</a>
</h3>
<span class="tenure">2010-01 — Present</span>
<p>Super-Ultra-gold level Ultimate Access MSDN subscriber package with subscription
toaster and XBox ping pong racket.</p>
</div>
<div>
<h3><em>Coordinator</em>,
Campus Coder&#39;s Meetup
</h3>
<span class="tenure">2003-02 — 2004-04</span>
<p>Host of a monthly <strong>campus-wide meetup for CS students</strong>.
Code, coffee, and crullers!</p>
</div>
</section>
<hr>
<section id="samples">
<header> <span class="fa fa-lg fa-share"></span>
<h2>samples</h2>
</header>
<div>
<h3>
<a href="http://janef.me/asteroids">Asteroids</a>
</h3>
<span class="tenure">2014-09</span>
<p>A browser-based space shooter built on Three.js.</p>
</div>
<div>
<h3>
<a href="https://rememberpedia.com">Rememberpedia</a>
</h3>
<span class="tenure">2015-07</span>
<p>A website to help you remember things.</p>
</div>
</section>
<hr>
<section id="writing">
<header> <span class="fa fa-lg fa-pencil"></span>
<h2>writing</h2>
</header>
<div>
<h3><em><a href="http://codeproject.com/build-ui-electron-atom.aspx">Building User Interfaces with Electron and Atom</a></em>,
Code Project</h3>
<span class="tenure">2011</span>
</div>
<div>
<h3><em><a href="http://janef.me">Jane Fullstacker&#39;s Blog</a></em>,
self</h3>
<span class="tenure">2011</span>
</div>
<div>
<h3><em><a href="http://url.to.publication.com/blah">Teach Yourself GORFF in 21 Days</a></em>,
Amazon</h3>
<span class="tenure">2008</span>
<p>A primer on the programming language of GORFF, whose for loops are coterminous
with all of time and space.</p>
</div>
</section>
<hr>
<section id="reading">
<header> <span class="fa fa-lg fa-book"></span>
<h2>reading</h2>
</header>
<div>
<h3><em><a href="https://www.reddit.com/r/programming/">r/programming</a></em></h3>
<span class="tenure">Current</span>
<p>Daily reader and longtime lurker.</p>
</div>
<div>
<h3><em><a href="https://news.ycombinator.com/">Hacker News / YCombinator</a></em></h3>
<span class="tenure">Current</span>
<p>Daily reader and longtime lurker.</p>
</div>
<div>
<h3><em><a href="http://www.codinghorror.com">Coding Horror</a></em>, Jeff Atwood</h3>
<span class="tenure">Current</span>
<p>Reader since 2007; member of the StackOverflow Beta.</p>
</div>
<div>
<h3><em><a href="http://www.cc2e.com/Default.aspx">Code Complete</a></em>, Steve McConnell</h3>
<span class="tenure">2014</span>
<p>My &#39;desert-island&#39; software construction manual.</p>
</div>
</section>
<hr>
<section id="recognition">
<header> <span class="fa fa-lg fa-trophy"></span>
<h2>recognition</h2>
</header>
<div>
<h3><em>Honorable Mention</em>, Google</h3>
<span class="tenure">2012</span>
</div>
<div>
<h3><em>Summa cum laude</em>, Cornell University</h3>
<span class="tenure">2012</span>
</div>
</section>
<hr>
<section id="speaking">
<header> <span class="fa fa-lg fa-users"></span>
<h2>speaking</h2>
</header>
<div>
<h3><em>Data Warehousing Evolved: DARMA 2.0</em>, OPENSTART 2013 Developer&#39;s Conference</h3>
<span class="tenure">2012</span>
<ul>
<li>Won &#39;Best Presentation on an Emerging Technical Field&#39; prize.</li>
</ul>
</div>
</section>
<hr>
<section id="testimonials">
<header> <span class="fa fa-lg fa-quote-left"></span>
<h2>testimonials</h2>
</header>
<div>
<h3><em>John Davidson</em></h3>
<p>Jane is awesome! I&#39;d hire her again in a heartbeat.</p>
</div>
<div>
<h3><em>Dana Nevins</em></h3>
<p>I&#39;ve known Jane personally and professionally for almost ten years.
She is one in a million.</p>
</div>
</section>
<hr>
<section id="interests">
<header> <span class="fa fa-lg fa-bicycle"></span>
<h2>interests</h2>
</header>
<div>
<h3><em>reading</em></h3>
<span class="tenure">Current</span>

View File

@ -2,7 +2,7 @@
"basics": { "basics": {
"name": "Jane Q. Fullstacker", "name": "Jane Q. Fullstacker",
"label": "Senior Developer", "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.", "summary": "**Imaginary full-stack software developer with 6+ years industry experience** specializing in cloud-driven web applications and middleware. A native of southern CA, Jane enjoys hiking, mystery novels, and the company of Rufus, her two year old beagle.",
"website": "http://janef.me/blog", "website": "http://janef.me/blog",
"phone": "1-650-999-7777", "phone": "1-650-999-7777",
"email": "jdoe@onecoolstartup.io", "email": "jdoe@onecoolstartup.io",
@ -29,11 +29,11 @@
}, },
"work": [ "work": [
{ {
"company": "One Cool Startup", "company": "Area 52",
"website": "https://onecool.io/does-not-exist", "website": "https://area52.io/does-not-exist",
"position": "Head Code Ninja", "position": "Head Code Ninja",
"startDate": "2013-09", "startDate": "2013-09",
"summary": "Development team manager for OneCoolApp and OneCoolWebsite, a free social network tiddlywink generator and lifestyle portal with over 200,000 users.", "summary": "Development team manager for [**Quantum Diorama**](https://en.wikipedia.org/wiki/Vaporware), a distributed molecular modeling and analysis suite for Linux and OS X.",
"highlights": [ "highlights": [
"Managed a 5-person development team", "Managed a 5-person development team",
"Accomplishment 2", "Accomplishment 2",
@ -104,7 +104,8 @@
], ],
"skills": [ "skills": [
{ {
"name": "Web Dev", "name": "Web",
"level": "advanced",
"keywords": [ "keywords": [
"JavaScript", "JavaScript",
"HTML 5", "HTML 5",
@ -116,6 +117,7 @@
}, },
{ {
"name": "JavaScript", "name": "JavaScript",
"level": "master",
"keywords": [ "keywords": [
"Node.js", "Node.js",
"Angular.js", "Angular.js",
@ -127,6 +129,7 @@
}, },
{ {
"name": "Database", "name": "Database",
"level": "intermediate",
"keywords": [ "keywords": [
"MySQL", "MySQL",
"PostgreSQL", "PostgreSQL",
@ -137,6 +140,7 @@
}, },
{ {
"name": "Cloud", "name": "Cloud",
"level": "intermediate",
"keywords": [ "keywords": [
"AWS", "AWS",
"EC2", "EC2",
@ -148,9 +152,12 @@
}, },
{ {
"name": "Project", "name": "Project",
"level": "beginner",
"keywords": [ "keywords": [
"Agile", "Agile",
"TFS", "TFS",
"JIRA",
"GitHub",
"Unified Process", "Unified Process",
"MS Project" "MS Project"
] ]
@ -158,6 +165,7 @@
], ],
"volunteer": [ "volunteer": [
{ {
"flavor": "volunteer",
"organization": "Technology for Tots", "organization": "Technology for Tots",
"position": "Technical Consultant", "position": "Technical Consultant",
"startDate": "2003-11", "startDate": "2003-11",
@ -171,6 +179,7 @@
] ]
}, },
{ {
"flavor": "military",
"organization": "US Army Reserves", "organization": "US Army Reserves",
"position": "NCO", "position": "NCO",
"startDate": "1999-11", "startDate": "1999-11",
@ -186,11 +195,13 @@
], ],
"awards": [ "awards": [
{ {
"flavor": "award",
"title": "Honorable Mention", "title": "Honorable Mention",
"date": "2012", "date": "2012",
"awarder": "Google" "awarder": "Google"
}, },
{ {
"flavor": "honor",
"title": "Summa cum laude", "title": "Summa cum laude",
"date": "2012", "date": "2012",
"awarder": "Cornell University" "awarder": "Cornell University"
@ -240,10 +251,6 @@
"name": "John Davidson", "name": "John Davidson",
"reference": "Jane is awesome! I'd hire her again in a heartbeat." "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", "name": "Dana Nevins",
"reference": "I've known Jane personally and professionally for almost ten years. She is one in a million." "reference": "I've known Jane personally and professionally for almost ten years. She is one in a million."
@ -259,5 +266,53 @@
"level": "Moderate", "level": "Moderate",
"years": 10 "years": 10
} }
],
"projects": [
{
"name": "HackMyResume",
"description": "A resume authoring and analysis tool for OS X, Linux, and Windows.",
"keywords": [
"JavaScript",
"Node.js",
"cross-platform",
"JSON"
],
"startDate": "2015-09",
"url": "https://fluentdesk.com/hackmyresume",
"roles": [
"contributor"
]
},
{
"name": "Augmented Android",
"description": "An augmented reality app for Android.",
"keywords": [
"Android",
"Java",
"Xamarin"
],
"startDate": "2012-02",
"endDate": "2014-01",
"url": "http://project.website.com",
"roles": [
"co-creator"
]
},
{
"name": "Blog",
"description": "My programming blog. Powered by Jekyll.",
"keywords": [
"Jekyll",
"Ruby",
"HTML 5",
"JavaScript",
"HTTP",
"LAMP"
],
"url": "http://myblog.jane.com/blog",
"roles": [
"creator"
]
}
] ]
} }

View File

@ -0,0 +1,194 @@
Jane Q. Fullstacker
============
Email: jdoe@onecoolstartup.io
Tel: 1-650-999-7777
Web: http://janef.me/blog
**Imaginary full-stack software developer with 6+ years industry experience** specializing in cloud-driven web applications and middleware. A native of southern CA, Jane enjoys hiking, mystery novels, and the company of Rufus, her two year old beagle.
## SKILLS
- Web: JavaScript HTML 5 CSS LAMP MVC REST
- JavaScript: Node.js Angular.js jQuery Bootstrap React.js Backbone.js
- Database: MySQL PostgreSQL NoSQL ORM Hibernate
- Cloud: AWS EC2 RDS S3 Azure Dropbox
- Project: Agile TFS JIRA GitHub Unified Process MS Project
## EMPLOYMENT
### *Head Code Ninja*, [Area 52](https://area52.io/does-not-exist) (2013-09 — Present)
Development team manager for [**Quantum Diorama**](https://en.wikipedia.org/wiki/Vaporware), a distributed molecular modeling and analysis suite for Linux and OS X.
- Managed a 5-person development team
- Accomplishment 2
- Etc.
### *Principal Developer*, [Veridian Dynamics](https://en.wikipedia.org/wiki/Better_Off_Ted#Plot) (2011-07 — 2013-08)
Developer on numerous projects culminating in technical lead role for the [Jabberwocky project](http://betteroffted.wikia.com/wiki/Jabberwocky) and promotion to principal developer.
- Managed a 5-person development team
- Accomplishment 2
- Etc.
### *IT Administrator*, Stark Industries (2008-10 — 2011-06)
Junior programmer with heavy code responsibilities. Promoted to intermediate role after 6 months.
- Promoted to intermediate developer after 6 months
- Accomplishment 2
- Etc.
### *Intern*, Dunder Mifflin (2008-06 — 2008-09)
Performed IT administration and deployments for Dunder Mifflin.
- Supervised roll-out of Dunder Mifflin Infinity website.
- Performed mission-critical system backups and
- Etc.
## PROJECTS
### *Contributor*, [HackMyResume](https://fluentdesk.com/hackmyresume) (2015-09 — Present)
A resume authoring and analysis tool for OS X, Linux, and Windows.
Exemplar user for HackMyResume and FluentCV!
### *Co-creator*, [Augmented Android](http://project.website.com) (2012-02 — 2014-01)
An augmented reality app for Android.
Performed flagship product conceptualization and development.
### *Creator*, [Blog](http://myblog.jane.com/blog) (??? — Present)
My programming blog. Powered by Jekyll.
Conceptualization, design, development, and deployment.
## GOVERNANCE
### *Member*, [The Mommies Network](http://themommiesnetwork.org)
Since 2008 I've been a full-time member of the board of directors for TMN.
### *Academic Contributor*, [Khronos Group](https://www.khronos.org)
- Participated in GORFF standardization process (Draft 2).
## EDUCATION
### [Cornell University](https://www.cornell.edu/) (2005-09 — 2008-05)
A multiline summary of the education.
### [Medfield College](https://en.wikipedia.org/wiki/Medfield_College) (2003-09 — 2005-06)
A multiline summary of the education.
## AFFILIATION
### *Member*, [IEEE](https://www.ieee.org/index.html) (2013-06 — Present)
Member in good standing since 2013-06.
### *Member*, [Apple Developer Network](https://developer.apple.com/) (??? — Present)
Member of the [Apple Developer program](https://developer.apple.com/) since 2008.
### *Subscriber*, [MSDN](https://msdn.microsoft.com) (2010-01 — Present)
Super-Ultra-gold level Ultimate Access MSDN subscriber package with subscription toaster and XBox ping pong racket.
### *Coordinator*, Campus Coder's Meetup (2003-02 — 2004-04)
Host of a monthly **campus-wide meetup for CS students**. Code, coffee, and crullers!
## SAMPLES
### [Asteroids](http://janef.me/asteroids) (2014-09)
A browser-based space shooter built on Three.js.
### [Rememberpedia](https://rememberpedia.com) (2015-07)
A website to help you remember things.
## WRITING
### [Building User Interfaces with Electron and Atom](http://codeproject.com/build-ui-electron-atom.aspx) (2011-01)
### [Jane Fullstacker's Blog](http://janef.me) (2011-01)
### [Teach Yourself GORFF in 21 Days](http://url.to.publication.com/blah) (2008-01)
A primer on the programming language of GORFF, whose for loops are coterminous with all of time and space.
## READING
### [*r/programming*](https://www.reddit.com/r/programming/)
Daily reader and longtime lurker.
### [*Hacker News / YCombinator*](https://news.ycombinator.com/)
Daily reader and longtime lurker.
### [*Coding Horror*](http://www.codinghorror.com), Jeff Atwood
Reader since 2007; member of the StackOverflow Beta.
### [*Code Complete*](http://www.cc2e.com/Default.aspx), Steve McConnell
My 'desert-island' software construction manual.
## SERVICE
### *Technical Consultant*, [Technology for Tots](http://technology-for-tots.org) (2003-11 — 2005-06)
Summary of this volunteer stint.
- Accomplishment 1
- Accomplishment 2
- etc
### *NCO*, [US Army Reserves](http://www.usar.army.mil/) (1999-11 — 2003-06)
Summary of this military stint.
- Accomplishment 1
- Accomplishment 2
- etc
## RECOGNITION
### Honorable Mention, Google (Jan 2012)
### Summa cum laude, Cornell University (Jan 2012)
## SPEAKING
### *Data Warehousing Evolved: DARMA 2.0*, OPENSTART 2013 Developer's Conference (2012)
- Won 'Best Presentation on an Emerging Technical Field' prize.
## INTERESTS
- READING: mystery Agatha Christie John Grisham
Jane is a fan of mystery novels and courtroom dramas including Agatha Christie and John Grisham.
- HIKING
Jane enjoys hiking, light mountain climbing, and has four summits under her belt!
- YOGA

Binary file not shown.

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